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 ;
1922use PHPStan \Type \Type ;
2023use PHPStan \Type \TypeCombinator ;
2124use PHPStan \Type \TypeUtils ;
25+ use PHPStan \Type \UnionType ;
2226use function array_key_exists ;
2327use function count ;
2428use function in_array ;
@@ -86,6 +90,12 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
8690 return TypeUtils::toBenevolentUnion ($ defaultReturnType );
8791 }
8892
93+ $ stringOrArray = new UnionType ([new StringType (), new ArrayType (new MixedType (), new MixedType ())]);
94+ if (!$ stringOrArray ->isSuperTypeOf ($ subjectArgumentType )->yes ()) {
95+ return $ defaultReturnType ;
96+ }
97+
98+ $ replaceArgumentType = null ;
8999 if (array_key_exists ($ functionReflection ->getName (), self ::FUNCTIONS_REPLACE_POSITION )) {
90100 $ replaceArgumentPosition = self ::FUNCTIONS_REPLACE_POSITION [$ functionReflection ->getName ()];
91101
@@ -94,68 +104,88 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
94104 if ($ replaceArgumentType ->isArray ()->yes ()) {
95105 $ replaceArgumentType = $ replaceArgumentType ->getIterableValueType ();
96106 }
107+ }
108+ }
97109
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- }
110+ $ result = [];
104111
105- if ($ subjectArgumentType ->isLowercaseString ()->yes () && $ replaceArgumentType ->isLowercaseString ()->yes ()) {
106- $ accessories [] = new AccessoryLowercaseStringType ();
107- }
112+ $ stringArgumentType = TypeCombinator::intersect (new StringType (), $ subjectArgumentType );
113+ if ($ stringArgumentType ->isString ()->yes ()) {
114+ $ result [] = $ this ->getReplaceType ($ stringArgumentType , $ replaceArgumentType );
115+ }
108116
109- if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
110- $ accessories [] = new AccessoryUppercaseStringType ();
117+ $ arrayArgumentType = TypeCombinator::intersect (new ArrayType (new MixedType (), new MixedType ()), $ subjectArgumentType );
118+ if ($ arrayArgumentType ->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 = $ arrayArgumentType ->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 ();
111139 }
112-
113- if (count ($ accessories ) > 0 ) {
114- $ accessories [] = new StringType ();
115- return new IntersectionType ($ accessories );
140+ } else {
141+ $ newArrayType = new ArrayType (
142+ $ arrayArgumentType ->getIterableKeyType (),
143+ $ this ->getReplaceType ($ arrayArgumentType ->getIterableValueType (), $ replaceArgumentType ),
144+ );
145+ if ($ arrayArgumentType ->isList ()->yes ()) {
146+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new AccessoryArrayListType ());
116147 }
148+ if ($ arrayArgumentType ->isIterableAtLeastOnce ()->yes ()) {
149+ $ newArrayType = TypeCombinator::intersect ($ newArrayType , new NonEmptyArrayType ());
150+ }
151+
152+ $ result [] = $ newArrayType ;
117153 }
118154 }
119155
120- $ isStringSuperType = $ subjectArgumentType ->isString ();
121- $ isArraySuperType = $ subjectArgumentType ->isArray ();
122- $ compareSuperTypes = $ isStringSuperType ->compareTo ($ isArraySuperType );
123- 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 ) {
124165 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- }
166+ }
149167
150- $ result [] = $ arrayType ->generalizeValues ();
151- }
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+ }
152174
153- return TypeCombinator::union (...$ result );
154- }
155- 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 );
156186 }
157187
158- return $ defaultReturnType ;
188+ return new StringType () ;
159189 }
160190
161191 private function getSubjectType (
0 commit comments