66use PHPStan \Analyser \Scope ;
77use PHPStan \Reflection \FunctionReflection ;
88use PHPStan \Reflection \ParametersAcceptorSelector ;
9+ use PHPStan \Type \Accessory \AccessoryArrayListType ;
910use PHPStan \Type \Accessory \AccessoryLowercaseStringType ;
1011use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
1112use PHPStan \Type \Accessory \AccessoryNonFalsyStringType ;
1213use PHPStan \Type \Accessory \AccessoryUppercaseStringType ;
14+ use PHPStan \Type \Accessory \NonEmptyArrayType ;
15+ use PHPStan \Type \ArrayType ;
1316use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
1417use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
1518use PHPStan \Type \IntersectionType ;
1619use PHPStan \Type \MixedType ;
20+ use PHPStan \Type \NeverType ;
1721use PHPStan \Type \StringType ;
1822use PHPStan \Type \Type ;
1923use PHPStan \Type \TypeCombinator ;
2024use PHPStan \Type \TypeUtils ;
25+ use PHPStan \Type \UnionType ;
2126use function array_key_exists ;
2227use function count ;
2328use function in_array ;
@@ -84,6 +89,12 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
8489 return TypeUtils::toBenevolentUnion ($ defaultReturnType );
8590 }
8691
92+ $ stringOrArray = new UnionType ([new StringType (), new ArrayType (new MixedType (), new MixedType ())]);
93+ if (!$ stringOrArray ->isSuperTypeOf ($ subjectArgumentType )->yes ()) {
94+ return $ defaultReturnType ;
95+ }
96+
97+ $ replaceArgumentType = null ;
8798 if (array_key_exists ($ functionReflection ->getName (), self ::FUNCTIONS_REPLACE_POSITION )) {
8899 $ replaceArgumentPosition = self ::FUNCTIONS_REPLACE_POSITION [$ functionReflection ->getName ()];
89100
@@ -92,68 +103,89 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
92103 if ($ replaceArgumentType ->isArray ()->yes ()) {
93104 $ replaceArgumentType = $ replaceArgumentType ->getIterableValueType ();
94105 }
106+ }
107+ }
95108
96- $ accessories = [];
97- if ($ subjectArgumentType ->isNonFalsyString ()->yes () && $ replaceArgumentType ->isNonFalsyString ()->yes ()) {
98- $ accessories [] = new AccessoryNonFalsyStringType ();
99- } elseif ($ subjectArgumentType ->isNonEmptyString ()->yes () && $ replaceArgumentType ->isNonEmptyString ()->yes ()) {
100- $ accessories [] = new AccessoryNonEmptyStringType ();
101- }
109+ $ result = [];
102110
103- if ( $ subjectArgumentType -> isLowercaseString ()-> yes () && $ replaceArgumentType -> isLowercaseString ()-> yes ()) {
104- $ accessories [] = new AccessoryLowercaseStringType ();
105- }
111+ $ stringArgumentType = TypeCombinator:: intersect ( new StringType (), $ subjectArgumentType );
112+ if (! $ stringArgumentType instanceof NeverType) {
113+ $ result [] = $ this -> getReplaceType ( $ stringArgumentType , $ replaceArgumentType );
106114
107- if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
108- $ accessories [] = new AccessoryUppercaseStringType ();
109- }
115+ $ subjectArgumentType = $ subjectArgumentType ->tryRemove ($ stringArgumentType );
116+ }
110117
111- if (count ($ accessories ) > 0 ) {
112- $ accessories [] = new StringType ();
113- return new IntersectionType ($ accessories );
118+ if ($ subjectArgumentType ->isArray ()->yes ()) {
119+ $ keyShouldBeOptional = in_array (
120+ $ functionReflection ->getName (),
121+ ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ],
122+ true ,
123+ );
124+
125+ $ constantArrays = $ subjectArgumentType ->getConstantArrays ();
126+ if ($ constantArrays !== []) {
127+ foreach ($ constantArrays as $ constantArray ) {
128+ $ valueTypes = $ constantArray ->getValueTypes ();
129+
130+ $ builder = ConstantArrayTypeBuilder::createEmpty ();
131+ foreach ($ constantArray ->getKeyTypes () as $ index => $ keyType ) {
132+ $ builder ->setOffsetValueType (
133+ $ keyType ,
134+ $ this ->getReplaceType ($ valueTypes [$ index ], $ replaceArgumentType ),
135+ $ keyShouldBeOptional || $ constantArray ->isOptionalKey ($ index ),
136+ );
137+ }
138+ $ result [] = $ builder ->getArray ();
114139 }
140+ } else {
141+ $ newArrayType = new ArrayType (
142+ $ subjectArgumentType ->getIterableKeyType (),
143+ $ this ->getReplaceType ($ subjectArgumentType ->getIterableValueType (), $ replaceArgumentType ),
144+ );
145+ if ($ subjectArgumentType ->isList ()->yes ()) {
146+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new AccessoryArrayListType ());
147+ }
148+ if ($ subjectArgumentType ->isIterableAtLeastOnce ()->yes ()) {
149+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new NonEmptyArrayType ());
150+ }
151+
152+ $ result [] = $ newArrayType ;
115153 }
116154 }
117155
118- $ isStringSuperType = $ subjectArgumentType ->isString ();
119- $ isArraySuperType = $ subjectArgumentType ->isArray ();
120- $ compareSuperTypes = $ isStringSuperType ->compareTo ($ isArraySuperType );
121- if ($ compareSuperTypes === $ isStringSuperType ) {
156+ return TypeCombinator::union (...$ result );
157+ }
158+
159+ private function getReplaceType (
160+ Type $ subjectArgumentType ,
161+ ?Type $ replaceArgumentType ,
162+ ): Type
163+ {
164+ if ($ replaceArgumentType === null ) {
122165 return new StringType ();
123- } elseif ($ compareSuperTypes === $ isArraySuperType ) {
124- $ subjectArrays = $ subjectArgumentType ->getArrays ();
125- if (count ($ subjectArrays ) > 0 ) {
126- $ result = [];
127- foreach ($ subjectArrays as $ arrayType ) {
128- $ constantArrays = $ arrayType ->getConstantArrays ();
129-
130- if (
131- $ constantArrays !== []
132- && in_array ($ functionReflection ->getName (), ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ], true )
133- ) {
134- foreach ($ constantArrays as $ constantArray ) {
135- $ generalizedArray = $ constantArray ->generalizeValues ();
136-
137- $ builder = ConstantArrayTypeBuilder::createEmpty ();
138- // turn all keys optional
139- foreach ($ constantArray ->getKeyTypes () as $ keyType ) {
140- $ builder ->setOffsetValueType ($ keyType , $ generalizedArray ->getOffsetValueType ($ keyType ), true );
141- }
142- $ result [] = $ builder ->getArray ();
143- }
144-
145- continue ;
146- }
166+ }
147167
148- $ result [] = $ arrayType ->generalizeValues ();
149- }
168+ $ accessories = [];
169+ if ($ subjectArgumentType ->isNonFalsyString ()->yes () && $ replaceArgumentType ->isNonFalsyString ()->yes ()) {
170+ $ accessories [] = new AccessoryNonFalsyStringType ();
171+ } elseif ($ subjectArgumentType ->isNonEmptyString ()->yes () && $ replaceArgumentType ->isNonEmptyString ()->yes ()) {
172+ $ accessories [] = new AccessoryNonEmptyStringType ();
173+ }
150174
151- return TypeCombinator::union (...$ result );
152- }
153- return $ subjectArgumentType ;
175+ if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
176+ $ accessories [] = new AccessoryLowercaseStringType ();
177+ }
178+
179+ if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
180+ $ accessories [] = new AccessoryUppercaseStringType ();
181+ }
182+
183+ if (count ($ accessories ) > 0 ) {
184+ $ accessories [] = new StringType ();
185+ return new IntersectionType ($ accessories );
154186 }
155187
156- return $ defaultReturnType ;
188+ return new StringType () ;
157189 }
158190
159191 private function getSubjectType (
0 commit comments