7979use PHPStan \Node \Expr \GetIterableKeyTypeExpr ;
8080use PHPStan \Node \Expr \GetIterableValueTypeExpr ;
8181use PHPStan \Node \Expr \OriginalForeachKeyExpr ;
82+ use PHPStan \Node \Expr \OriginalForeachValueExpr ;
8283use PHPStan \Node \Expr \PropertyInitializationExpr ;
8384use PHPStan \Node \Expr \TypeExpr ;
8485use PHPStan \Node \Expr \UnsetOffsetExpr ;
@@ -1330,10 +1331,27 @@ public function processStmtNode(
13301331 && $ exprType ->isConstantArray ()->no ()
13311332 ) {
13321333 $ arrayExprDimFetch = new ArrayDimFetch ($ stmt ->expr , $ stmt ->keyVar );
1334+ $ originalValueExpr = null ;
1335+ if ($ stmt ->valueVar instanceof Variable && is_string ($ stmt ->valueVar ->name )) {
1336+ $ originalValueExpr = new OriginalForeachValueExpr ($ stmt ->valueVar ->name );
1337+ }
13331338 $ arrayDimFetchLoopTypes = [];
13341339 $ keyLoopTypes = [];
13351340 foreach ($ scopesWithIterableValueType as $ scopeWithIterableValueType ) {
1336- $ arrayDimFetchLoopTypes [] = $ scopeWithIterableValueType ->getType ($ arrayExprDimFetch );
1341+ $ dimFetchType = $ scopeWithIterableValueType ->getType ($ arrayExprDimFetch );
1342+ // Condition-based narrowings like `is_string($type)` apply to the value
1343+ // variable but not automatically to the array dim fetch, even though the
1344+ // two describe the same element for a given iteration. If the value var
1345+ // hasn't been reassigned (OriginalForeachValueExpr still tracked) we use
1346+ // the narrowed value-var type in place of the broader dim fetch type so
1347+ // the loop's final array rewrite below picks up the sharper element type.
1348+ if ($ originalValueExpr !== null && $ scopeWithIterableValueType ->hasExpressionType ($ originalValueExpr )->yes ()) {
1349+ $ valueVarType = $ scopeWithIterableValueType ->getType ($ stmt ->valueVar );
1350+ if ($ dimFetchType ->isSuperTypeOf ($ valueVarType )->yes ()) {
1351+ $ dimFetchType = $ valueVarType ;
1352+ }
1353+ }
1354+ $ arrayDimFetchLoopTypes [] = $ dimFetchType ;
13371355 $ keyLoopTypes [] = $ scopeWithIterableValueType ->getType ($ stmt ->keyVar );
13381356 }
13391357
@@ -1343,8 +1361,15 @@ public function processStmtNode(
13431361 $ arrayDimFetchLoopNativeTypes = [];
13441362 $ keyLoopNativeTypes = [];
13451363 foreach ($ scopesWithIterableValueType as $ scopeWithIterableValueType ) {
1346- $ arrayDimFetchLoopNativeTypes [] = $ scopeWithIterableValueType ->getNativeType ($ arrayExprDimFetch );
1347- $ keyLoopNativeTypes [] = $ scopeWithIterableValueType ->getNativeType ($ stmt ->keyVar );
1364+ $ dimFetchNativeType = $ scopeWithIterableValueType ->getNativeType ($ arrayExprDimFetch );
1365+ if ($ originalValueExpr !== null && $ scopeWithIterableValueType ->hasExpressionType ($ originalValueExpr )->yes ()) {
1366+ $ valueVarNativeType = $ scopeWithIterableValueType ->getNativeType ($ stmt ->valueVar );
1367+ if ($ dimFetchNativeType ->isSuperTypeOf ($ valueVarNativeType )->yes ()) {
1368+ $ dimFetchNativeType = $ valueVarNativeType ;
1369+ }
1370+ }
1371+ $ arrayDimFetchLoopNativeTypes [] = $ dimFetchNativeType ;
1372+ $ keyLoopNativeTypes [] = $ scopeWithIterableValueType ->getType ($ stmt ->keyVar );
13481373 }
13491374
13501375 $ arrayDimFetchLoopNativeType = TypeCombinator::union (...$ arrayDimFetchLoopNativeTypes );
@@ -3911,6 +3936,11 @@ private function tryProcessUnrolledConstantArrayForeach(
39113936 $ nativeValueType ,
39123937 TrinaryLogic::createYes (),
39133938 );
3939+ $ iterScope = $ iterScope ->assignExpression (
3940+ new OriginalForeachValueExpr ($ valueVarName ),
3941+ $ valueType ,
3942+ $ nativeValueType ,
3943+ );
39143944 if ($ keyVarName !== null ) {
39153945 $ iterScope = $ iterScope ->assignVariable (
39163946 $ keyVarName ,
0 commit comments