77use PHPStan \DependencyInjection \AutowiredService ;
88use PHPStan \Reflection \FunctionReflection ;
99use PHPStan \Reflection \ParametersAcceptorSelector ;
10+ use PHPStan \Type \Accessory \AccessoryArrayListType ;
1011use PHPStan \Type \Accessory \AccessoryLowercaseStringType ;
1112use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
1213use PHPStan \Type \Accessory \AccessoryNonFalsyStringType ;
1314use PHPStan \Type \Accessory \AccessoryUppercaseStringType ;
15+ use PHPStan \Type \Accessory \NonEmptyArrayType ;
16+ use PHPStan \Type \ArrayType ;
1417use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
1518use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
1619use PHPStan \Type \IntersectionType ;
@@ -86,6 +89,7 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
8689 return TypeUtils::toBenevolentUnion ($ defaultReturnType );
8790 }
8891
92+ $ replaceArgumentType = null ;
8993 if (array_key_exists ($ functionReflection ->getName (), self ::FUNCTIONS_REPLACE_POSITION )) {
9094 $ replaceArgumentPosition = self ::FUNCTIONS_REPLACE_POSITION [$ functionReflection ->getName ()];
9195
@@ -94,68 +98,96 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
9498 if ($ replaceArgumentType ->isArray ()->yes ()) {
9599 $ replaceArgumentType = $ replaceArgumentType ->getIterableValueType ();
96100 }
101+ }
102+ }
97103
98- $ accessories = [];
99- if ($ subjectArgumentType ->isNonFalsyString ()->yes () && $ replaceArgumentType ->isNonFalsyString ()->yes ()) {
100- $ accessories [] = new AccessoryNonFalsyStringType ();
101- } elseif ($ subjectArgumentType ->isNonEmptyString ()->yes () && $ replaceArgumentType ->isNonEmptyString ()->yes ()) {
102- $ accessories [] = new AccessoryNonEmptyStringType ();
103- }
104+ $ result = [];
104105
105- if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
106- $ accessories [] = new AccessoryLowercaseStringType ();
107- }
106+ if ($ subjectArgumentType ->isString ()->yes ()) {
107+ $ stringArgumentType = $ subjectArgumentType ;
108+ } else {
109+ $ stringArgumentType = TypeCombinator::intersect (new StringType (), $ subjectArgumentType );
110+ }
111+ if ($ stringArgumentType ->isString ()->yes ()) {
112+ $ result [] = $ this ->getReplaceType ($ stringArgumentType , $ replaceArgumentType );
113+ }
108114
109- if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
110- $ accessories [] = new AccessoryUppercaseStringType ();
115+ if ($ subjectArgumentType ->isArray ()->yes ()) {
116+ $ arrayArgumentType = $ subjectArgumentType ;
117+ } else {
118+ $ arrayArgumentType = TypeCombinator::intersect (new ArrayType (new MixedType (), new MixedType ()), $ subjectArgumentType );
119+ }
120+ if ($ arrayArgumentType ->isArray ()->yes ()) {
121+ $ keyShouldBeOptional = in_array (
122+ $ functionReflection ->getName (),
123+ ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ],
124+ true ,
125+ );
126+
127+ $ constantArrays = $ arrayArgumentType ->getConstantArrays ();
128+ if ($ constantArrays !== []) {
129+ foreach ($ constantArrays as $ constantArray ) {
130+ $ valueTypes = $ constantArray ->getValueTypes ();
131+
132+ $ builder = ConstantArrayTypeBuilder::createEmpty ();
133+ foreach ($ constantArray ->getKeyTypes () as $ index => $ keyType ) {
134+ $ builder ->setOffsetValueType (
135+ $ keyType ,
136+ $ this ->getReplaceType ($ valueTypes [$ index ], $ replaceArgumentType ),
137+ $ keyShouldBeOptional || $ constantArray ->isOptionalKey ($ index ),
138+ );
139+ }
140+ $ result [] = $ builder ->getArray ();
111141 }
112-
113- if (count ($ accessories ) > 0 ) {
114- $ accessories [] = new StringType ();
115- return new IntersectionType ($ accessories );
142+ } else {
143+ $ newArrayType = new ArrayType (
144+ $ arrayArgumentType ->getIterableKeyType (),
145+ $ this ->getReplaceType ($ arrayArgumentType ->getIterableValueType (), $ replaceArgumentType ),
146+ );
147+ if ($ arrayArgumentType ->isList ()->yes ()) {
148+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new AccessoryArrayListType ());
116149 }
150+ if ($ arrayArgumentType ->isIterableAtLeastOnce ()->yes ()) {
151+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new NonEmptyArrayType ());
152+ }
153+
154+ $ result [] = $ newArrayType ;
117155 }
118156 }
119157
120- $ isStringSuperType = $ subjectArgumentType ->isString ();
121- $ isArraySuperType = $ subjectArgumentType ->isArray ();
122- $ compareSuperTypes = $ isStringSuperType ->compareTo ($ isArraySuperType );
123- if ($ compareSuperTypes === $ isStringSuperType ) {
158+ return TypeCombinator::union (...$ result );
159+ }
160+
161+ private function getReplaceType (
162+ Type $ subjectArgumentType ,
163+ ?Type $ replaceArgumentType ,
164+ ): Type
165+ {
166+ if ($ replaceArgumentType === null ) {
124167 return new StringType ();
125- } elseif ($ compareSuperTypes === $ isArraySuperType ) {
126- $ subjectArrays = $ subjectArgumentType ->getArrays ();
127- if (count ($ subjectArrays ) > 0 ) {
128- $ result = [];
129- foreach ($ subjectArrays as $ arrayType ) {
130- $ constantArrays = $ arrayType ->getConstantArrays ();
131-
132- if (
133- $ constantArrays !== []
134- && in_array ($ functionReflection ->getName (), ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ], true )
135- ) {
136- foreach ($ constantArrays as $ constantArray ) {
137- $ generalizedArray = $ constantArray ->generalizeValues ();
138-
139- $ builder = ConstantArrayTypeBuilder::createEmpty ();
140- // turn all keys optional
141- foreach ($ constantArray ->getKeyTypes () as $ keyType ) {
142- $ builder ->setOffsetValueType ($ keyType , $ generalizedArray ->getOffsetValueType ($ keyType ), true );
143- }
144- $ result [] = $ builder ->getArray ();
145- }
146-
147- continue ;
148- }
168+ }
149169
150- $ result [] = $ arrayType ->generalizeValues ();
151- }
170+ $ accessories = [];
171+ if ($ subjectArgumentType ->isNonFalsyString ()->yes () && $ replaceArgumentType ->isNonFalsyString ()->yes ()) {
172+ $ accessories [] = new AccessoryNonFalsyStringType ();
173+ } elseif ($ subjectArgumentType ->isNonEmptyString ()->yes () && $ replaceArgumentType ->isNonEmptyString ()->yes ()) {
174+ $ accessories [] = new AccessoryNonEmptyStringType ();
175+ }
152176
153- return TypeCombinator::union (...$ result );
154- }
155- return $ subjectArgumentType ;
177+ if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
178+ $ accessories [] = new AccessoryLowercaseStringType ();
179+ }
180+
181+ if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
182+ $ accessories [] = new AccessoryUppercaseStringType ();
183+ }
184+
185+ if (count ($ accessories ) > 0 ) {
186+ $ accessories [] = new StringType ();
187+ return new IntersectionType ($ accessories );
156188 }
157189
158- return $ defaultReturnType ;
190+ return new StringType () ;
159191 }
160192
161193 private function getSubjectType (
0 commit comments