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 ;
1821use PHPStan \Type \Type ;
1922use PHPStan \Type \TypeCombinator ;
2023use PHPStan \Type \TypeUtils ;
24+ use PHPStan \Type \UnionType ;
2125use function array_key_exists ;
2226use function count ;
2327use function in_array ;
@@ -84,6 +88,12 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
8488 return TypeUtils::toBenevolentUnion ($ defaultReturnType );
8589 }
8690
91+ $ stringOrArray = new UnionType ([new StringType (), new ArrayType (new MixedType (), new MixedType ())]);
92+ if (!$ stringOrArray ->isSuperTypeOf ($ subjectArgumentType )->yes ()) {
93+ return $ defaultReturnType ;
94+ }
95+
96+ $ replaceArgumentType = null ;
8797 if (array_key_exists ($ functionReflection ->getName (), self ::FUNCTIONS_REPLACE_POSITION )) {
8898 $ replaceArgumentPosition = self ::FUNCTIONS_REPLACE_POSITION [$ functionReflection ->getName ()];
8999
@@ -92,68 +102,88 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
92102 if ($ replaceArgumentType ->isArray ()->yes ()) {
93103 $ replaceArgumentType = $ replaceArgumentType ->getIterableValueType ();
94104 }
105+ }
106+ }
95107
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- }
108+ $ result = [];
102109
103- if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
104- $ accessories [] = new AccessoryLowercaseStringType ();
105- }
110+ $ stringArgumentType = TypeCombinator::intersect (new StringType (), $ subjectArgumentType );
111+ if ($ stringArgumentType ->isString ()->yes ()) {
112+ $ result [] = $ this ->getReplaceType ($ stringArgumentType , $ replaceArgumentType );
113+ }
106114
107- if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
108- $ accessories [] = new AccessoryUppercaseStringType ();
115+ $ arrayArgumentType = TypeCombinator::intersect (new ArrayType (new MixedType (), new MixedType ()), $ subjectArgumentType );
116+ if ($ arrayArgumentType ->isArray ()->yes ()) {
117+ $ keyShouldBeOptional = in_array (
118+ $ functionReflection ->getName (),
119+ ['preg_replace ' , 'preg_replace_callback ' , 'preg_replace_callback_array ' ],
120+ true ,
121+ );
122+
123+ $ constantArrays = $ arrayArgumentType ->getConstantArrays ();
124+ if ($ constantArrays !== []) {
125+ foreach ($ constantArrays as $ constantArray ) {
126+ $ valueTypes = $ constantArray ->getValueTypes ();
127+
128+ $ builder = ConstantArrayTypeBuilder::createEmpty ();
129+ foreach ($ constantArray ->getKeyTypes () as $ index => $ keyType ) {
130+ $ builder ->setOffsetValueType (
131+ $ keyType ,
132+ $ this ->getReplaceType ($ valueTypes [$ index ], $ replaceArgumentType ),
133+ $ keyShouldBeOptional || $ constantArray ->isOptionalKey ($ index ),
134+ );
135+ }
136+ $ result [] = $ builder ->getArray ();
109137 }
110-
111- if (count ($ accessories ) > 0 ) {
112- $ accessories [] = new StringType ();
113- return new IntersectionType ($ accessories );
138+ } else {
139+ $ newArrayType = new ArrayType (
140+ $ arrayArgumentType ->getIterableKeyType (),
141+ $ this ->getReplaceType ($ arrayArgumentType ->getIterableValueType (), $ replaceArgumentType ),
142+ );
143+ if ($ arrayArgumentType ->isList ()->yes ()) {
144+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new AccessoryArrayListType ());
114145 }
146+ if ($ arrayArgumentType ->isIterableAtLeastOnce ()->yes ()) {
147+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new NonEmptyArrayType ());
148+ }
149+
150+ $ result [] = $ newArrayType ;
115151 }
116152 }
117153
118- $ isStringSuperType = $ subjectArgumentType ->isString ();
119- $ isArraySuperType = $ subjectArgumentType ->isArray ();
120- $ compareSuperTypes = $ isStringSuperType ->compareTo ($ isArraySuperType );
121- if ($ compareSuperTypes === $ isStringSuperType ) {
154+ return TypeCombinator::union (...$ result );
155+ }
156+
157+ private function getReplaceType (
158+ Type $ subjectArgumentType ,
159+ ?Type $ replaceArgumentType ,
160+ ): Type
161+ {
162+ if ($ replaceArgumentType === null ) {
122163 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- }
164+ }
147165
148- $ result [] = $ arrayType ->generalizeValues ();
149- }
166+ $ accessories = [];
167+ if ($ subjectArgumentType ->isNonFalsyString ()->yes () && $ replaceArgumentType ->isNonFalsyString ()->yes ()) {
168+ $ accessories [] = new AccessoryNonFalsyStringType ();
169+ } elseif ($ subjectArgumentType ->isNonEmptyString ()->yes () && $ replaceArgumentType ->isNonEmptyString ()->yes ()) {
170+ $ accessories [] = new AccessoryNonEmptyStringType ();
171+ }
150172
151- return TypeCombinator::union (...$ result );
152- }
153- return $ subjectArgumentType ;
173+ if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
174+ $ accessories [] = new AccessoryLowercaseStringType ();
175+ }
176+
177+ if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
178+ $ accessories [] = new AccessoryUppercaseStringType ();
179+ }
180+
181+ if (count ($ accessories ) > 0 ) {
182+ $ accessories [] = new StringType ();
183+ return new IntersectionType ($ accessories );
154184 }
155185
156- return $ defaultReturnType ;
186+ return new StringType () ;
157187 }
158188
159189 private function getSubjectType (
0 commit comments