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,90 @@ 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 );
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+ $ arrayArgumentType = TypeCombinator::intersect (new ArrayType (new MixedType (), new MixedType ()), $ subjectArgumentType );
119+ if ($ arrayArgumentType ->isArray ()->yes ()) {
120+ $ keyShouldBeOptional = in_array (
121+ $ functionReflection ->getName (),
122+ ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ],
123+ true ,
124+ );
125+
126+ $ constantArrays = $ arrayArgumentType ->getConstantArrays ();
127+ if ($ constantArrays !== []) {
128+ foreach ($ constantArrays as $ constantArray ) {
129+ $ valueTypes = $ constantArray ->getValueTypes ();
130+
131+ $ builder = ConstantArrayTypeBuilder::createEmpty ();
132+ foreach ($ constantArray ->getKeyTypes () as $ index => $ keyType ) {
133+ $ builder ->setOffsetValueType (
134+ $ keyType ,
135+ $ this ->getReplaceType ($ valueTypes [$ index ], $ replaceArgumentType ),
136+ $ keyShouldBeOptional || $ constantArray ->isOptionalKey ($ index ),
137+ );
138+ }
139+ $ result [] = $ builder ->getArray ();
114140 }
141+ } else {
142+ $ newArrayType = new ArrayType (
143+ $ arrayArgumentType ->getIterableKeyType (),
144+ $ this ->getReplaceType ($ arrayArgumentType ->getIterableValueType (), $ replaceArgumentType ),
145+ );
146+ if ($ arrayArgumentType ->isList ()->yes ()) {
147+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new AccessoryArrayListType ());
148+ }
149+ if ($ arrayArgumentType ->isIterableAtLeastOnce ()->yes ()) {
150+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new NonEmptyArrayType ());
151+ }
152+
153+ $ result [] = $ newArrayType ;
115154 }
116155 }
117156
118- $ isStringSuperType = $ subjectArgumentType ->isString ();
119- $ isArraySuperType = $ subjectArgumentType ->isArray ();
120- $ compareSuperTypes = $ isStringSuperType ->compareTo ($ isArraySuperType );
121- if ($ compareSuperTypes === $ isStringSuperType ) {
157+ return TypeCombinator::union (...$ result );
158+ }
159+
160+ private function getReplaceType (
161+ Type $ subjectArgumentType ,
162+ ?Type $ replaceArgumentType ,
163+ ): Type
164+ {
165+ if ($ replaceArgumentType === null ) {
122166 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- }
167+ }
147168
148- $ result [] = $ arrayType ->generalizeValues ();
149- }
169+ $ accessories = [];
170+ if ($ subjectArgumentType ->isNonFalsyString ()->yes () && $ replaceArgumentType ->isNonFalsyString ()->yes ()) {
171+ $ accessories [] = new AccessoryNonFalsyStringType ();
172+ } elseif ($ subjectArgumentType ->isNonEmptyString ()->yes () && $ replaceArgumentType ->isNonEmptyString ()->yes ()) {
173+ $ accessories [] = new AccessoryNonEmptyStringType ();
174+ }
150175
151- return TypeCombinator::union (...$ result );
152- }
153- return $ subjectArgumentType ;
176+ if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
177+ $ accessories [] = new AccessoryLowercaseStringType ();
178+ }
179+
180+ if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
181+ $ accessories [] = new AccessoryUppercaseStringType ();
182+ }
183+
184+ if (count ($ accessories ) > 0 ) {
185+ $ accessories [] = new StringType ();
186+ return new IntersectionType ($ accessories );
154187 }
155188
156- return $ defaultReturnType ;
189+ return new StringType () ;
157190 }
158191
159192 private function getSubjectType (
0 commit comments