6
6
use PHPStan \Analyser \Scope ;
7
7
use PHPStan \Reflection \FunctionReflection ;
8
8
use PHPStan \Reflection \ParametersAcceptorSelector ;
9
+ use PHPStan \Type \Accessory \AccessoryArrayListType ;
9
10
use PHPStan \Type \Accessory \AccessoryLowercaseStringType ;
10
11
use PHPStan \Type \Accessory \AccessoryNonEmptyStringType ;
11
12
use PHPStan \Type \Accessory \AccessoryNonFalsyStringType ;
12
13
use PHPStan \Type \Accessory \AccessoryUppercaseStringType ;
14
+ use PHPStan \Type \Accessory \NonEmptyArrayType ;
15
+ use PHPStan \Type \ArrayType ;
13
16
use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
14
17
use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
15
18
use PHPStan \Type \IntersectionType ;
16
19
use PHPStan \Type \MixedType ;
20
+ use PHPStan \Type \NeverType ;
17
21
use PHPStan \Type \StringType ;
18
22
use PHPStan \Type \Type ;
19
23
use PHPStan \Type \TypeCombinator ;
20
24
use PHPStan \Type \TypeUtils ;
25
+ use PHPStan \Type \UnionType ;
21
26
use function array_key_exists ;
22
27
use function count ;
23
28
use function in_array ;
@@ -84,6 +89,12 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
84
89
return TypeUtils::toBenevolentUnion ($ defaultReturnType );
85
90
}
86
91
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 ;
87
98
if (array_key_exists ($ functionReflection ->getName (), self ::FUNCTIONS_REPLACE_POSITION )) {
88
99
$ replaceArgumentPosition = self ::FUNCTIONS_REPLACE_POSITION [$ functionReflection ->getName ()];
89
100
@@ -92,68 +103,90 @@ private function getPreliminarilyResolvedTypeFromFunctionCall(
92
103
if ($ replaceArgumentType ->isArray ()->yes ()) {
93
104
$ replaceArgumentType = $ replaceArgumentType ->getIterableValueType ();
94
105
}
106
+ }
107
+ }
95
108
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 = [];
102
110
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 );
106
114
107
- if ($ subjectArgumentType ->isUppercaseString ()->yes () && $ replaceArgumentType ->isUppercaseString ()->yes ()) {
108
- $ accessories [] = new AccessoryUppercaseStringType ();
109
- }
115
+ $ subjectArgumentType = $ subjectArgumentType ->tryRemove ($ stringArgumentType );
116
+ }
110
117
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 ();
114
140
}
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 ;
115
154
}
116
155
}
117
156
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 ) {
122
166
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
+ }
147
168
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
+ }
150
175
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 );
154
187
}
155
188
156
- return $ defaultReturnType ;
189
+ return new StringType () ;
157
190
}
158
191
159
192
private function getSubjectType (
0 commit comments