|
91 | 91 | use PHPStan\Node\Expr\GetIterableValueTypeExpr; |
92 | 92 | use PHPStan\Node\Expr\GetOffsetValueTypeExpr; |
93 | 93 | use PHPStan\Node\Expr\NativeTypeExpr; |
| 94 | +use PHPStan\Node\Expr\OriginalForeachKeyExpr; |
94 | 95 | use PHPStan\Node\Expr\OriginalPropertyTypeExpr; |
95 | 96 | use PHPStan\Node\Expr\PropertyInitializationExpr; |
96 | 97 | use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; |
@@ -1247,14 +1248,94 @@ private function processStmtNode( |
1247 | 1248 | $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt, $nodeCallback); |
1248 | 1249 | $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); |
1249 | 1250 | $finalScope = $finalScopeResult->getScope(); |
| 1251 | + $scopesWithIterableValueType = []; |
| 1252 | + |
| 1253 | + $originalKeyVarExpr = null; |
| 1254 | + $continueExitPointHasUnoriginalKeyType = false; |
| 1255 | + if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) { |
| 1256 | + $originalKeyVarExpr = new OriginalForeachKeyExpr($stmt->keyVar->name); |
| 1257 | + if ($finalScope->hasExpressionType($originalKeyVarExpr)->yes()) { |
| 1258 | + $scopesWithIterableValueType[] = $finalScope; |
| 1259 | + } else { |
| 1260 | + $continueExitPointHasUnoriginalKeyType = true; |
| 1261 | + } |
| 1262 | + } |
| 1263 | + |
1250 | 1264 | foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { |
1251 | | - $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); |
| 1265 | + $continueScope = $continueExitPoint->getScope(); |
| 1266 | + $finalScope = $continueScope->mergeWith($finalScope); |
| 1267 | + if ($originalKeyVarExpr === null || !$continueScope->hasExpressionType($originalKeyVarExpr)->yes()) { |
| 1268 | + $continueExitPointHasUnoriginalKeyType = true; |
| 1269 | + continue; |
| 1270 | + } |
| 1271 | + $scopesWithIterableValueType[] = $continueScope; |
1252 | 1272 | } |
1253 | | - foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { |
| 1273 | + $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class); |
| 1274 | + foreach ($breakExitPoints as $breakExitPoint) { |
1254 | 1275 | $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); |
1255 | 1276 | } |
1256 | 1277 |
|
1257 | 1278 | $exprType = $scope->getType($stmt->expr); |
| 1279 | + $hasExpr = $scope->hasExpressionType($stmt->expr); |
| 1280 | + if ( |
| 1281 | + count($breakExitPoints) === 0 |
| 1282 | + && count($scopesWithIterableValueType) > 0 |
| 1283 | + && !$continueExitPointHasUnoriginalKeyType |
| 1284 | + && $stmt->keyVar !== null |
| 1285 | + && $exprType->isArray()->yes() |
| 1286 | + && $exprType->isConstantArray()->no() |
| 1287 | + && !$hasExpr->no() |
| 1288 | + ) { |
| 1289 | + $arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $stmt->keyVar); |
| 1290 | + $arrayDimFetchLoopTypes = []; |
| 1291 | + foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) { |
| 1292 | + $arrayDimFetchLoopTypes[] = $scopeWithIterableValueType->getType($arrayExprDimFetch); |
| 1293 | + } |
| 1294 | + |
| 1295 | + $arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes); |
| 1296 | + |
| 1297 | + $arrayDimFetchLoopNativeTypes = []; |
| 1298 | + foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) { |
| 1299 | + $arrayDimFetchLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch); |
| 1300 | + } |
| 1301 | + |
| 1302 | + $arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes); |
| 1303 | + |
| 1304 | + if (!$arrayDimFetchLoopType->equals($exprType->getIterableValueType())) { |
| 1305 | + $newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType): Type { |
| 1306 | + if ($type instanceof UnionType || $type instanceof IntersectionType) { |
| 1307 | + return $traverse($type); |
| 1308 | + } |
| 1309 | + |
| 1310 | + if (!$type instanceof ArrayType) { |
| 1311 | + return $type; |
| 1312 | + } |
| 1313 | + |
| 1314 | + return new ArrayType($type->getKeyType(), $arrayDimFetchLoopType); |
| 1315 | + }); |
| 1316 | + $newExprNativeType = TypeTraverser::map($scope->getNativeType($stmt->expr), static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType): Type { |
| 1317 | + if ($type instanceof UnionType || $type instanceof IntersectionType) { |
| 1318 | + return $traverse($type); |
| 1319 | + } |
| 1320 | + |
| 1321 | + if (!$type instanceof ArrayType) { |
| 1322 | + return $type; |
| 1323 | + } |
| 1324 | + |
| 1325 | + return new ArrayType($type->getKeyType(), $arrayDimFetchLoopNativeType); |
| 1326 | + }); |
| 1327 | + |
| 1328 | + if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { |
| 1329 | + $finalScope = $finalScope->assignVariable( |
| 1330 | + $stmt->expr->name, |
| 1331 | + $newExprType, |
| 1332 | + $newExprNativeType, |
| 1333 | + $hasExpr, |
| 1334 | + ); |
| 1335 | + } |
| 1336 | + } |
| 1337 | + } |
| 1338 | + |
1258 | 1339 | $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce(); |
1259 | 1340 | if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) { |
1260 | 1341 | $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr( |
|
0 commit comments