Skip to content

Commit

Permalink
Resolve key and value type of partially non-iterable types when enter…
Browse files Browse the repository at this point in the history
…ing foreach
  • Loading branch information
ondrejmirtes committed Jun 29, 2023
1 parent 9a0bc5e commit cb5a2b4
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 58 deletions.
68 changes: 58 additions & 10 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down Expand Up @@ -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, [
Expand Down Expand Up @@ -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, [
Expand Down Expand Up @@ -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);
}
Expand All @@ -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),
);
}

Expand Down Expand Up @@ -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();
}

}
4 changes: 4 additions & 0 deletions src/Analyser/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/Dependency/DependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
53 changes: 7 additions & 46 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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),
];
}
}
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
21 changes: 21 additions & 0 deletions tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace ForeachPartiallyNonIterable;

use function PHPStan\Testing\assertType;

class Foo
{

/**
* @param array<string, int>|false $a
*/
public function doFoo($a): void
{
foreach ($a as $k => $v) {
assertType('string', $k);
assertType('int', $v);
}
}

}

0 comments on commit cb5a2b4

Please sign in to comment.