@@ -1347,6 +1347,10 @@ private function processStmtNode(
13471347 $ finalScope = $ breakExitPoint ->getScope ()->mergeWith ($ finalScope );
13481348 }
13491349
1350+ if ($ context ->isTopLevel ()) {
1351+ $ finalScope = $ this ->refineForEachScopeForConstantArray ($ finalScope , $ scope , $ originalStorage , $ stmt , $ context , $ breakExitPoints , $ arrayComparisonExpr );
1352+ }
1353+
13501354 $ exprType = $ scope ->getType ($ stmt ->expr );
13511355 $ hasExpr = $ scope ->hasExpressionType ($ stmt ->expr );
13521356 if (
@@ -7078,6 +7082,106 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto
70787082 return $ this ->processVarAnnotation ($ scope , $ vars , $ stmt );
70797083 }
70807084
7085+ /**
7086+ * @param InternalStatementExitPoint[] $breakExitPoints
7087+ */
7088+ private function refineForEachScopeForConstantArray (
7089+ MutatingScope $ finalScope ,
7090+ MutatingScope $ outerScope ,
7091+ ExpressionResultStorage $ originalStorage ,
7092+ Foreach_ $ stmt ,
7093+ StatementContext $ context ,
7094+ array $ breakExitPoints ,
7095+ Expr $ arrayComparisonExpr ,
7096+ ): MutatingScope
7097+ {
7098+ if (count ($ breakExitPoints ) > 0 ) {
7099+ return $ finalScope ;
7100+ }
7101+
7102+ if ($ stmt ->byRef ) {
7103+ return $ finalScope ;
7104+ }
7105+
7106+ if ($ stmt ->getDocComment () !== null ) {
7107+ return $ finalScope ;
7108+ }
7109+
7110+ if (!$ stmt ->valueVar instanceof Variable || !is_string ($ stmt ->valueVar ->name )) {
7111+ return $ finalScope ;
7112+ }
7113+
7114+ if (!$ stmt ->keyVar instanceof Variable || !is_string ($ stmt ->keyVar ->name )) {
7115+ return $ finalScope ;
7116+ }
7117+
7118+ $ iterateeType = $ outerScope ->getType ($ stmt ->expr );
7119+ $ nativeIterateeType = $ outerScope ->getNativeType ($ stmt ->expr );
7120+ $ constantArrays = $ iterateeType ->getConstantArrays ();
7121+ $ nativeConstantArrays = $ nativeIterateeType ->getConstantArrays ();
7122+ if (
7123+ !$ iterateeType ->isConstantArray ()->yes ()
7124+ || count ($ constantArrays ) !== 1
7125+ || !$ iterateeType ->isIterableAtLeastOnce ()->yes ()
7126+ ) {
7127+ return $ finalScope ;
7128+ }
7129+
7130+ $ constantArray = $ constantArrays [0 ];
7131+ $ nativeConstantArray = count ($ nativeConstantArrays ) === 1 ? $ nativeConstantArrays [0 ] : null ;
7132+
7133+ $ keyTypes = $ constantArray ->getKeyTypes ();
7134+ if (count ($ keyTypes ) === 0 ) {
7135+ return $ finalScope ;
7136+ }
7137+
7138+ // Process the loop body element-by-element with specific key/value types
7139+ $ unrolledScope = $ this ->polluteScopeWithAlwaysIterableForeach ? $ outerScope ->filterByTruthyValue ($ arrayComparisonExpr ) : $ outerScope ;
7140+ foreach ($ keyTypes as $ i => $ keyType ) {
7141+ $ valueType = $ constantArray ->getValueTypes ()[$ i ];
7142+ $ nativeKeyType = $ nativeConstantArray !== null ? $ nativeConstantArray ->getKeyTypes ()[$ i ] : $ keyType ;
7143+ $ nativeValueType = $ nativeConstantArray !== null ? $ nativeConstantArray ->getValueTypes ()[$ i ] : $ valueType ;
7144+
7145+ $ elementScope = $ unrolledScope ->assignVariable (
7146+ $ stmt ->keyVar ->name ,
7147+ $ keyType ,
7148+ $ nativeKeyType ,
7149+ TrinaryLogic::createYes (),
7150+ );
7151+ $ elementScope = $ elementScope ->assignVariable (
7152+ $ stmt ->valueVar ->name ,
7153+ $ valueType ,
7154+ $ nativeValueType ,
7155+ TrinaryLogic::createYes (),
7156+ );
7157+
7158+ $ elementStorage = $ originalStorage ->duplicate ();
7159+ $ elementResult = $ this ->processStmtNodesInternal (
7160+ $ stmt ,
7161+ $ stmt ->stmts ,
7162+ $ elementScope ,
7163+ $ elementStorage ,
7164+ new NoopNodeCallback (),
7165+ $ context ->enterDeep (),
7166+ )->filterOutLoopExitPoints ();
7167+
7168+ if (count ($ elementResult ->getExitPointsByType (Break_::class)) > 0 ) {
7169+ return $ finalScope ;
7170+ }
7171+
7172+ if ($ elementResult ->isAlwaysTerminating ()) {
7173+ return $ finalScope ;
7174+ }
7175+
7176+ $ unrolledScope = $ elementResult ->getScope ();
7177+ foreach ($ elementResult ->getExitPointsByType (Continue_::class) as $ continueExitPoint ) {
7178+ $ unrolledScope = $ continueExitPoint ->getScope ()->mergeWith ($ unrolledScope );
7179+ }
7180+ }
7181+
7182+ return $ finalScope ->refineTypesFromConstantArrayForeach ($ unrolledScope );
7183+ }
7184+
70817185 /**
70827186 * @param callable(Node $node, Scope $scope): void $nodeCallback
70837187 */
0 commit comments