@@ -714,6 +714,17 @@ public function specifyTypesInCondition(
714714 if (!$ scope instanceof MutatingScope) {
715715 throw new ShouldNotHappenException ();
716716 }
717+
718+ // For deep BooleanAnd chains in truthy context, flatten and
719+ // process all arms at once to avoid O(N²) recursive
720+ // filterByTruthyValue calls.
721+ if (
722+ $ context ->true ()
723+ && BooleanAndHandler::getBooleanExpressionDepth ($ expr ) > self ::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH
724+ ) {
725+ return $ this ->specifyTypesForFlattenedBooleanAnd ($ scope , $ expr , $ context );
726+ }
727+
717728 $ leftTypes = $ this ->specifyTypesInCondition ($ scope , $ expr ->left , $ context )->setRootExpr ($ expr );
718729 $ rightScope = $ scope ->filterByTruthyValue ($ expr ->left );
719730 $ rightTypes = $ this ->specifyTypesInCondition ($ rightScope , $ expr ->right , $ context )->setRootExpr ($ expr );
@@ -2020,13 +2031,36 @@ private function specifyTypesForFlattenedBooleanOr(
20202031 $ arms = array_reverse ($ arms );
20212032
20222033 if ($ context ->false () || $ context ->falsey ()) {
2023- // Falsey: all arms are false → union all SpecifiedTypes
2024- $ result = new SpecifiedTypes ([], []);
2034+ // Falsey: all arms are false → union all SpecifiedTypes.
2035+ // Collect per-expression types first, then build unions once
2036+ // to avoid O(N²) from incremental TypeCombinator::union() growth.
2037+ /** @var array<string, array{Expr, list<Type>}> $sureTypesPerExpr */
2038+ $ sureTypesPerExpr = [];
2039+ /** @var array<string, array{Expr, list<Type>}> $sureNotTypesPerExpr */
2040+ $ sureNotTypesPerExpr = [];
2041+
20252042 foreach ($ arms as $ arm ) {
20262043 $ armTypes = $ this ->specifyTypesInCondition ($ scope , $ arm , $ context );
2027- $ result = $ result ->unionWith ($ armTypes );
2044+ foreach ($ armTypes ->getSureTypes () as $ exprString => [$ exprNode , $ type ]) {
2045+ $ sureTypesPerExpr [$ exprString ][0 ] = $ exprNode ;
2046+ $ sureTypesPerExpr [$ exprString ][1 ][] = $ type ;
2047+ }
2048+ foreach ($ armTypes ->getSureNotTypes () as $ exprString => [$ exprNode , $ type ]) {
2049+ $ sureNotTypesPerExpr [$ exprString ][0 ] = $ exprNode ;
2050+ $ sureNotTypesPerExpr [$ exprString ][1 ][] = $ type ;
2051+ }
2052+ }
2053+
2054+ $ sureTypes = [];
2055+ foreach ($ sureTypesPerExpr as $ exprString => [$ exprNode , $ types ]) {
2056+ $ sureTypes [$ exprString ] = [$ exprNode , TypeCombinator::intersect (...$ types )];
20282057 }
2029- return $ result ->setRootExpr ($ expr );
2058+ $ sureNotTypes = [];
2059+ foreach ($ sureNotTypesPerExpr as $ exprString => [$ exprNode , $ types ]) {
2060+ $ sureNotTypes [$ exprString ] = [$ exprNode , TypeCombinator::union (...$ types )];
2061+ }
2062+
2063+ return (new SpecifiedTypes ($ sureTypes , $ sureNotTypes ))->setRootExpr ($ expr );
20302064 }
20312065
20322066 // Truthy: at least one arm is true → intersect all normalized SpecifiedTypes
@@ -2052,6 +2086,56 @@ private function specifyTypesForFlattenedBooleanOr(
20522086 return $ result ->setRootExpr ($ expr );
20532087 }
20542088
2089+ /**
2090+ * @param BooleanAnd|LogicalAnd $expr
2091+ */
2092+ private function specifyTypesForFlattenedBooleanAnd (
2093+ MutatingScope $ scope ,
2094+ Expr $ expr ,
2095+ TypeSpecifierContext $ context ,
2096+ ): SpecifiedTypes
2097+ {
2098+ $ arms = [];
2099+ $ current = $ expr ;
2100+ while ($ current instanceof BooleanAnd || $ current instanceof LogicalAnd) {
2101+ $ arms [] = $ current ->right ;
2102+ $ current = $ current ->left ;
2103+ }
2104+ $ arms [] = $ current ;
2105+ $ arms = array_reverse ($ arms );
2106+
2107+ // Truthy: all arms are true → union all SpecifiedTypes.
2108+ // Collect per-expression types first, then build unions once
2109+ // to avoid O(N²) from incremental growth.
2110+ /** @var array<string, array{Expr, list<Type>}> $sureTypesPerExpr */
2111+ $ sureTypesPerExpr = [];
2112+ /** @var array<string, array{Expr, list<Type>}> $sureNotTypesPerExpr */
2113+ $ sureNotTypesPerExpr = [];
2114+
2115+ foreach ($ arms as $ arm ) {
2116+ $ armTypes = $ this ->specifyTypesInCondition ($ scope , $ arm , $ context );
2117+ foreach ($ armTypes ->getSureTypes () as $ exprString => [$ exprNode , $ type ]) {
2118+ $ sureTypesPerExpr [$ exprString ][0 ] = $ exprNode ;
2119+ $ sureTypesPerExpr [$ exprString ][1 ][] = $ type ;
2120+ }
2121+ foreach ($ armTypes ->getSureNotTypes () as $ exprString => [$ exprNode , $ type ]) {
2122+ $ sureNotTypesPerExpr [$ exprString ][0 ] = $ exprNode ;
2123+ $ sureNotTypesPerExpr [$ exprString ][1 ][] = $ type ;
2124+ }
2125+ }
2126+
2127+ $ sureTypes = [];
2128+ foreach ($ sureTypesPerExpr as $ exprString => [$ exprNode , $ types ]) {
2129+ $ sureTypes [$ exprString ] = [$ exprNode , TypeCombinator::union (...$ types )];
2130+ }
2131+ $ sureNotTypes = [];
2132+ foreach ($ sureNotTypesPerExpr as $ exprString => [$ exprNode , $ types ]) {
2133+ $ sureNotTypes [$ exprString ] = [$ exprNode , TypeCombinator::union (...$ types )];
2134+ }
2135+
2136+ return (new SpecifiedTypes ($ sureTypes , $ sureNotTypes ))->setRootExpr ($ expr );
2137+ }
2138+
20552139 /**
20562140 * @return array<string, ConditionalExpressionHolder[]>
20572141 */
0 commit comments