Skip to content

Commit cb5a2b4

Browse files
committed
Resolve key and value type of partially non-iterable types when entering foreach
1 parent 9a0bc5e commit cb5a2b4

File tree

6 files changed

+94
-58
lines changed

6 files changed

+94
-58
lines changed

src/Analyser/MutatingScope.php

+58-10
Original file line numberDiff line numberDiff line change
@@ -618,10 +618,10 @@ public function getAnonymousFunctionReturnType(): ?Type
618618
public function getType(Expr $node): Type
619619
{
620620
if ($node instanceof GetIterableKeyTypeExpr) {
621-
return $this->getType($node->getExpr())->getIterableKeyType();
621+
return $this->getIterableKeyType($this->getType($node->getExpr()));
622622
}
623623
if ($node instanceof GetIterableValueTypeExpr) {
624-
return $this->getType($node->getExpr())->getIterableValueType();
624+
return $this->getIterableValueType($this->getType($node->getExpr()));
625625
}
626626
if ($node instanceof GetOffsetValueTypeExpr) {
627627
return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim()));
@@ -1201,8 +1201,8 @@ private function resolveType(string $exprString, Expr $node): Type
12011201
}
12021202
} else {
12031203
$yieldFromType = $arrowScope->getType($yieldNode->expr);
1204-
$keyType = $yieldFromType->getIterableKeyType();
1205-
$valueType = $yieldFromType->getIterableValueType();
1204+
$keyType = $arrowScope->getIterableKeyType($yieldFromType);
1205+
$valueType = $arrowScope->getIterableValueType($yieldFromType);
12061206
}
12071207

12081208
$returnType = new GenericObjectType(Generator::class, [
@@ -1305,8 +1305,8 @@ private function resolveType(string $exprString, Expr $node): Type
13051305
}
13061306

13071307
$yieldFromType = $yieldScope->getType($yieldNode->expr);
1308-
$keyTypes[] = $yieldFromType->getIterableKeyType();
1309-
$valueTypes[] = $yieldFromType->getIterableValueType();
1308+
$keyTypes[] = $yieldScope->getIterableKeyType($yieldFromType);
1309+
$valueTypes[] = $yieldScope->getIterableValueType($yieldFromType);
13101310
}
13111311

13121312
$returnType = new GenericObjectType(Generator::class, [
@@ -3115,7 +3115,11 @@ public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName
31153115
{
31163116
$iterateeType = $this->getType($iteratee);
31173117
$nativeIterateeType = $this->getNativeType($iteratee);
3118-
$scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType(), $nativeIterateeType->getIterableValueType());
3118+
$scope = $this->assignVariable(
3119+
$valueName,
3120+
$this->getIterableValueType($iterateeType),
3121+
$this->getIterableValueType($nativeIterateeType),
3122+
);
31193123
if ($keyName !== null) {
31203124
$scope = $scope->enterForeachKey($iteratee, $keyName);
31213125
}
@@ -3127,13 +3131,17 @@ public function enterForeachKey(Expr $iteratee, string $keyName): self
31273131
{
31283132
$iterateeType = $this->getType($iteratee);
31293133
$nativeIterateeType = $this->getNativeType($iteratee);
3130-
$scope = $this->assignVariable($keyName, $iterateeType->getIterableKeyType(), $nativeIterateeType->getIterableKeyType());
3134+
$scope = $this->assignVariable(
3135+
$keyName,
3136+
$this->getIterableKeyType($iterateeType),
3137+
$this->getIterableKeyType($nativeIterateeType),
3138+
);
31313139

31323140
if ($iterateeType->isArray()->yes()) {
31333141
$scope = $scope->assignExpression(
31343142
new Expr\ArrayDimFetch($iteratee, new Variable($keyName)),
3135-
$iterateeType->getIterableValueType(),
3136-
$nativeIterateeType->getIterableValueType(),
3143+
$this->getIterableValueType($iterateeType),
3144+
$this->getIterableValueType($nativeIterateeType),
31373145
);
31383146
}
31393147

@@ -5013,4 +5021,44 @@ private function getNativeConstantTypes(): array
50135021
return $constantTypes;
50145022
}
50155023

5024+
public function getIterableKeyType(Type $iteratee): Type
5025+
{
5026+
if ($iteratee instanceof UnionType) {
5027+
$newTypes = [];
5028+
foreach ($iteratee->getTypes() as $innerType) {
5029+
if (!$innerType->isIterable()->yes()) {
5030+
continue;
5031+
}
5032+
5033+
$newTypes[] = $innerType;
5034+
}
5035+
if (count($newTypes) === 0) {
5036+
return $iteratee->getIterableKeyType();
5037+
}
5038+
$iteratee = TypeCombinator::union(...$newTypes);
5039+
}
5040+
5041+
return $iteratee->getIterableKeyType();
5042+
}
5043+
5044+
public function getIterableValueType(Type $iteratee): Type
5045+
{
5046+
if ($iteratee instanceof UnionType) {
5047+
$newTypes = [];
5048+
foreach ($iteratee->getTypes() as $innerType) {
5049+
if (!$innerType->isIterable()->yes()) {
5050+
continue;
5051+
}
5052+
5053+
$newTypes[] = $innerType;
5054+
}
5055+
if (count($newTypes) === 0) {
5056+
return $iteratee->getIterableValueType();
5057+
}
5058+
$iteratee = TypeCombinator::union(...$newTypes);
5059+
}
5060+
5061+
return $iteratee->getIterableValueType();
5062+
}
5063+
50165064
}

src/Analyser/Scope.php

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ?
6363

6464
public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ConstantReflection;
6565

66+
public function getIterableKeyType(Type $iteratee): Type;
67+
68+
public function getIterableValueType(Type $iteratee): Type;
69+
6670
public function isInAnonymousFunction(): bool;
6771

6872
public function getAnonymousFunctionReflection(): ?ParametersAcceptor;

src/Dependency/DependencyResolver.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,13 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
404404
} elseif ($node instanceof Foreach_) {
405405
$exprType = $scope->getType($node->expr);
406406
if ($node->keyVar !== null) {
407-
foreach ($exprType->getIterableKeyType()->getReferencedClasses() as $referencedClass) {
407+
408+
foreach ($scope->getIterableKeyType($exprType)->getReferencedClasses() as $referencedClass) {
408409
$this->addClassToDependencies($referencedClass, $dependenciesReflections);
409410
}
410411
}
411412

412-
foreach ($exprType->getIterableValueType()->getReferencedClasses() as $referencedClass) {
413+
foreach ($scope->getIterableValueType($exprType)->getReferencedClasses() as $referencedClass) {
413414
$this->addClassToDependencies($referencedClass, $dependenciesReflections);
414415
}
415416
} elseif (

src/Reflection/ParametersAcceptorSelector.php

+7-46
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use PHPStan\Type\BooleanType;
1919
use PHPStan\Type\CallableType;
2020
use PHPStan\Type\Constant\ConstantIntegerType;
21-
use PHPStan\Type\ErrorType;
2221
use PHPStan\Type\Generic\TemplateType;
2322
use PHPStan\Type\Generic\TemplateTypeMap;
2423
use PHPStan\Type\IntegerType;
@@ -90,7 +89,7 @@ public static function selectFromArgs(
9089
$parameters = $acceptor->getParameters();
9190
$callbackParameters = [];
9291
foreach ($arrayMapArgs as $arg) {
93-
$callbackParameters[] = new DummyParameter('item', self::getIterableValueType($scope->getType($arg->value)), false, PassedByReference::createNo(), false, null);
92+
$callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($scope->getType($arg->value)), false, PassedByReference::createNo(), false, null);
9493
}
9594
$parameters[0] = new NativeParameterReflection(
9695
$parameters[0]->getName(),
@@ -151,12 +150,12 @@ public static function selectFromArgs(
151150
if ($mode instanceof ConstantIntegerType) {
152151
if ($mode->getValue() === ARRAY_FILTER_USE_KEY) {
153152
$arrayFilterParameters = [
154-
new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
153+
new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
155154
];
156155
} elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) {
157156
$arrayFilterParameters = [
158-
new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
159-
new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
157+
new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
158+
new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
160159
];
161160
}
162161
}
@@ -169,7 +168,7 @@ public static function selectFromArgs(
169168
$parameters[1]->isOptional(),
170169
new CallableType(
171170
$arrayFilterParameters ?? [
172-
new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
171+
new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
173172
],
174173
new MixedType(),
175174
false,
@@ -191,8 +190,8 @@ public static function selectFromArgs(
191190

192191
if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) {
193192
$arrayWalkParameters = [
194-
new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null),
195-
new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
193+
new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null),
194+
new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
196195
];
197196
if (isset($args[2])) {
198197
$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
548547
);
549548
}
550549

551-
private static function getIterableValueType(Type $type): Type
552-
{
553-
if ($type instanceof UnionType) {
554-
$types = [];
555-
foreach ($type->getTypes() as $innerType) {
556-
$iterableValueType = $innerType->getIterableValueType();
557-
if ($iterableValueType instanceof ErrorType) {
558-
continue;
559-
}
560-
561-
$types[] = $iterableValueType;
562-
}
563-
564-
return TypeCombinator::union(...$types);
565-
}
566-
567-
return $type->getIterableValueType();
568-
}
569-
570-
private static function getIterableKeyType(Type $type): Type
571-
{
572-
if ($type instanceof UnionType) {
573-
$types = [];
574-
foreach ($type->getTypes() as $innerType) {
575-
$iterableKeyType = $innerType->getIterableKeyType();
576-
if ($iterableKeyType instanceof ErrorType) {
577-
continue;
578-
}
579-
580-
$types[] = $iterableKeyType;
581-
}
582-
583-
return TypeCombinator::union(...$types);
584-
}
585-
586-
return $type->getIterableKeyType();
587-
}
588-
589550
private static function getCurlOptValueType(int $curlOpt): ?Type
590551
{
591552
if (defined('CURLOPT_SSL_VERIFYHOST') && $curlOpt === CURLOPT_SSL_VERIFYHOST) {

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,7 @@ public function dataFileAsserts(): iterable
12771277
yield from $this->gatherAssertTypes(__DIR__ . '/data/image-size.php');
12781278
yield from $this->gatherAssertTypes(__DIR__ . '/data/base64_decode.php');
12791279
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9404.php');
1280+
yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-partially-non-iterable.php');
12801281
yield from $this->gatherAssertTypes(__DIR__ . '/data/globals.php');
12811282
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9208.php');
12821283
yield from $this->gatherAssertTypes(__DIR__ . '/data/finite-types.php');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace ForeachPartiallyNonIterable;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param array<string, int>|false $a
12+
*/
13+
public function doFoo($a): void
14+
{
15+
foreach ($a as $k => $v) {
16+
assertType('string', $k);
17+
assertType('int', $v);
18+
}
19+
}
20+
21+
}

0 commit comments

Comments
 (0)