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,88 @@ 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 ->isString ()->yes ()) {
113+ $ result [] = $ this ->getReplaceType ($ stringArgumentType , $ replaceArgumentType );
114+ }
106115
107- if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
108- $ accessories [] = new AccessoryUppercaseStringType ();
116+ $ arrayArgumentType = TypeCombinator::intersect (new ArrayType (new MixedType (), new MixedType ()), $ subjectArgumentType );
117+ if ($ arrayArgumentType ->isArray ()->yes ()) {
118+ $ keyShouldBeOptional = in_array (
119+ $ functionReflection ->getName (),
120+ ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ],
121+ true ,
122+ );
123+
124+ $ constantArrays = $ arrayArgumentType ->getConstantArrays ();
125+ if ($ constantArrays !== []) {
126+ foreach ($ constantArrays as $ constantArray ) {
127+ $ valueTypes = $ constantArray ->getValueTypes ();
128+
129+ $ builder = ConstantArrayTypeBuilder::createEmpty ();
130+ foreach ($ constantArray ->getKeyTypes () as $ index => $ keyType ) {
131+ $ builder ->setOffsetValueType (
132+ $ keyType ,
133+ $ this ->getReplaceType ($ valueTypes [$ index ], $ replaceArgumentType ),
134+ $ keyShouldBeOptional || $ constantArray ->isOptionalKey ($ index ),
135+ );
136+ }
137+ $ result [] = $ builder ->getArray ();
109138 }
110-
111- if (count ($ accessories ) > 0 ) {
112- $ accessories [] = new StringType ();
113- return new IntersectionType ($ accessories );
139+ } else {
140+ $ newArrayType = new ArrayType (
141+ $ arrayArgumentType ->getIterableKeyType (),
142+ $ this ->getReplaceType ($ arrayArgumentType ->getIterableValueType (), $ replaceArgumentType ),
143+ );
144+ if ($ arrayArgumentType ->isList ()->yes ()) {
145+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new AccessoryArrayListType ());
114146 }
147+ if ($ arrayArgumentType ->isIterableAtLeastOnce ()->yes ()) {
148+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new NonEmptyArrayType ());
149+ }
150+
151+ $ result [] = $ newArrayType ;
115152 }
116153 }
117154
118- $ isStringSuperType = $ subjectArgumentType ->isString ();
119- $ isArraySuperType = $ subjectArgumentType ->isArray ();
120- $ compareSuperTypes = $ isStringSuperType ->compareTo ($ isArraySuperType );
121- if ($ compareSuperTypes === $ isStringSuperType ) {
155+ return TypeCombinator::union (...$ result );
156+ }
157+
158+ private function getReplaceType (
159+ Type $ subjectArgumentType ,
160+ ?Type $ replaceArgumentType ,
161+ ): Type
162+ {
163+ if ($ replaceArgumentType === null ) {
122164 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- }
165+ }
147166
148- $ result [] = $ arrayType ->generalizeValues ();
149- }
167+ $ accessories = [];
168+ if ($ subjectArgumentType ->isNonFalsyString ()->yes () && $ replaceArgumentType ->isNonFalsyString ()->yes ()) {
169+ $ accessories [] = new AccessoryNonFalsyStringType ();
170+ } elseif ($ subjectArgumentType ->isNonEmptyString ()->yes () && $ replaceArgumentType ->isNonEmptyString ()->yes ()) {
171+ $ accessories [] = new AccessoryNonEmptyStringType ();
172+ }
150173
151- return TypeCombinator::union (...$ result );
152- }
153- return $ subjectArgumentType ;
174+ if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
175+ $ accessories [] = new AccessoryLowercaseStringType ();
176+ }
177+
178+ if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
179+ $ accessories [] = new AccessoryUppercaseStringType ();
180+ }
181+
182+ if (count ($ accessories ) > 0 ) {
183+ $ accessories [] = new StringType ();
184+ return new IntersectionType ($ accessories );
154185 }
155186
156- return $ defaultReturnType ;
187+ return new StringType () ;
157188 }
158189
159190 private function getSubjectType (
0 commit comments