diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f616b644ab..945271fa84 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1315,21 +1315,30 @@ public function processStmtNode( ) { $arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $stmt->keyVar); $arrayDimFetchLoopTypes = []; + $keyLoopTypes = []; foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) { $arrayDimFetchLoopTypes[] = $scopeWithIterableValueType->getType($arrayExprDimFetch); + $keyLoopTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar); } $arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes); + $keyLoopType = TypeCombinator::union(...$keyLoopTypes); $arrayDimFetchLoopNativeTypes = []; + $keyLoopNativeTypes = []; foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) { $arrayDimFetchLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch); + $keyLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($stmt->keyVar); } $arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes); + $keyLoopNativeType = TypeCombinator::union(...$keyLoopNativeTypes); - if (!$arrayDimFetchLoopType->equals($exprType->getIterableValueType())) { - $newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType): Type { + $valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType()); + $keyTypeChanged = !$keyLoopType->equals($exprType->getIterableKeyType()); + + if ($valueTypeChanged || $keyTypeChanged) { + $newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType, $keyLoopType, $valueTypeChanged, $keyTypeChanged): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } @@ -1338,9 +1347,13 @@ public function processStmtNode( return $type; } - return new ArrayType($type->getKeyType(), $arrayDimFetchLoopType); + return new ArrayType( + $keyTypeChanged ? $keyLoopType : $type->getKeyType(), + $valueTypeChanged ? $arrayDimFetchLoopType : $type->getIterableValueType(), + ); }); - $newExprNativeType = TypeTraverser::map($scope->getNativeType($stmt->expr), static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType): Type { + $nativeExprType = $scope->getNativeType($stmt->expr); + $newExprNativeType = TypeTraverser::map($nativeExprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType, $keyLoopNativeType, $valueTypeChanged, $keyTypeChanged): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } @@ -1349,7 +1362,10 @@ public function processStmtNode( return $type; } - return new ArrayType($type->getKeyType(), $arrayDimFetchLoopNativeType); + return new ArrayType( + $keyTypeChanged ? $keyLoopNativeType : $type->getKeyType(), + $valueTypeChanged ? $arrayDimFetchLoopNativeType : $type->getIterableValueType(), + ); }); if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-7076.php b/tests/PHPStan/Analyser/nsrt/bug-7076.php new file mode 100644 index 0000000000..c05ead15b4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7076.php @@ -0,0 +1,202 @@ + $arguments + * @return array + */ +function narrowWithIsString(array $arguments): array +{ + foreach ($arguments as $key => $argument) { + if (!is_string($key)) { + throw new \Exception('Key must be a string'); + } + } + + assertType('array', $arguments); + + return $arguments; +} + +/** + * @param array $arguments + * @return array + */ +function narrowWithIsInt(array $arguments): array +{ + foreach ($arguments as $key => $argument) { + if (is_int($key)) { + throw new \Exception('Key must be a string'); + } + } + + assertType('array', $arguments); + + return $arguments; +} + +/** + * @param array $arguments + */ +function narrowToIntKeys(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + if (!is_int($key)) { + throw new \Exception('Key must be an int'); + } + } + + assertType('array', $arguments); +} + +/** + * @param array $arguments + */ +function narrowWithReturn(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + if (!is_string($key)) { + return; + } + } + + assertType('array', $arguments); +} + +/** + * @param array $arguments + */ +function continueDoesNotNarrow(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + if (!is_string($key)) { + continue; + } + } + + assertType('array', $arguments); +} + +/** + * @param array $arguments + */ +function breakPreventsNarrowing(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + if (!is_string($key)) { + throw new \Exception(); + } + if (rand(0, 1)) { + break; + } + } + + assertType('array', $arguments); +} + +/** + * @param array $arguments + */ +function keyAndValueNarrowing(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + if (!is_string($key)) { + throw new \Exception(); + } + $arguments[$key] = $argument ?? ''; + } + + assertType('array', $arguments); +} + +/** + * @param array $arguments + */ +function noKeyVar(array $arguments): void +{ + foreach ($arguments as $argument) { + if (!is_string($argument)) { + throw new \Exception(); + } + } + + assertType('array', $arguments); +} + +/** + * @param array $arguments + */ +function keyReassignedPreventsNarrowing(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + $key = 'test'; + if (!is_string($key)) { + throw new \Exception(); + } + } + + assertType('array', $arguments); +} + +/** + * @param array $arguments + */ +function narrowWithAssert(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + assert(is_string($key)); + } + + assertType('array', $arguments); +} + +/** + * @param non-empty-array $arguments + */ +function narrowNonEmptyArray(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + if (!is_string($key)) { + throw new \Exception(); + } + } + + assertType('non-empty-array', $arguments); +} + +class Foo +{ + /** @var array */ + private array $prop; + + public function narrowPropertyKey(): void + { + foreach ($this->prop as $k => $v) { + if (!is_string($k)) { + throw new \Exception(); + } + } + + assertType('array', $this->prop); + } +} + +/** + * @param array $arguments + */ +function partialContinueNarrowingDoesNotApply(array $arguments): void +{ + foreach ($arguments as $key => $argument) { + if (rand(0, 1)) { + continue; + } + if (!is_string($key)) { + throw new \Exception(); + } + } + + assertType('array', $arguments); +}