22
33namespace PHPStan \Type \Php ;
44
5- use PhpParser \Node \Arg ;
6- use PhpParser \Node \Expr ;
7- use PhpParser \Node \Expr \ArrowFunction ;
8- use PhpParser \Node \Expr \Closure ;
9- use PhpParser \Node \Expr \Error ;
105use PhpParser \Node \Expr \FuncCall ;
11- use PhpParser \Node \Expr \MethodCall ;
12- use PhpParser \Node \Expr \StaticCall ;
13- use PhpParser \Node \Expr \Variable ;
14- use PhpParser \Node \Name ;
15- use PhpParser \Node \Stmt \Return_ ;
16- use PHPStan \Analyser \MutatingScope ;
176use PHPStan \Analyser \Scope ;
187use PHPStan \Reflection \FunctionReflection ;
19- use PHPStan \Reflection \ReflectionProvider ;
20- use PHPStan \ShouldNotHappenException ;
21- use PHPStan \Type \ArrayType ;
22- use PHPStan \Type \BenevolentUnionType ;
23- use PHPStan \Type \Constant \ConstantArrayType ;
24- use PHPStan \Type \Constant \ConstantArrayTypeBuilder ;
25- use PHPStan \Type \Constant \ConstantBooleanType ;
26- use PHPStan \Type \Constant \ConstantIntegerType ;
278use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
28- use PHPStan \Type \ErrorType ;
29- use PHPStan \Type \MixedType ;
30- use PHPStan \Type \NeverType ;
31- use PHPStan \Type \NullType ;
32- use PHPStan \Type \StaticTypeFactory ;
339use PHPStan \Type \Type ;
34- use PHPStan \Type \TypeCombinator ;
35- use PHPStan \Type \TypeUtils ;
36- use function array_map ;
37- use function count ;
38- use function in_array ;
39- use function is_string ;
40- use function sprintf ;
41- use function substr ;
4210
4311final class ArrayFilterFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
4412{
4513
46- private const USE_BOTH = 1 ;
47- private const USE_KEY = 2 ;
48- private const USE_ITEM = 3 ;
49-
50- public function __construct (private ReflectionProvider $ reflectionProvider )
14+ public function __construct (private ArrayFilterFunctionReturnTypeHelper $ arrayFilterFunctionReturnTypeHelper )
5115 {
5216 }
5317
@@ -62,290 +26,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6226 $ callbackArg = $ functionCall ->getArgs ()[1 ]->value ?? null ;
6327 $ flagArg = $ functionCall ->getArgs ()[2 ]->value ?? null ;
6428
65- if ($ arrayArg === null ) {
66- return new ArrayType (new MixedType (), new MixedType ());
67- }
68-
69- $ arrayArgType = $ scope ->getType ($ arrayArg );
70- $ arrayArgType = TypeUtils::toBenevolentUnion ($ arrayArgType );
71- $ keyType = $ arrayArgType ->getIterableKeyType ();
72- $ itemType = $ arrayArgType ->getIterableValueType ();
73-
74- if ($ itemType instanceof NeverType || $ keyType instanceof NeverType) {
75- return new ConstantArrayType ([], []);
76- }
77-
78- if ($ arrayArgType instanceof MixedType) {
79- return new BenevolentUnionType ([
80- new ArrayType (new MixedType (), new MixedType ()),
81- new NullType (),
82- ]);
83- }
84-
85- if ($ callbackArg === null || $ scope ->getType ($ callbackArg )->isNull ()->yes ()) {
86- return TypeCombinator::union (
87- ...array_map ([$ this , 'removeFalsey ' ], $ arrayArgType ->getArrays ()),
88- );
89- }
90-
91- $ mode = $ this ->determineMode ($ flagArg , $ scope );
92- if ($ mode === null ) {
93- return new ArrayType ($ keyType , $ itemType );
94- }
95-
96- if ($ callbackArg instanceof Closure && count ($ callbackArg ->stmts ) === 1 && count ($ callbackArg ->params ) > 0 ) {
97- $ statement = $ callbackArg ->stmts [0 ];
98- if ($ statement instanceof Return_ && $ statement ->expr !== null ) {
99- if ($ mode === self ::USE_ITEM ) {
100- $ keyVar = null ;
101- $ itemVar = $ callbackArg ->params [0 ]->var ;
102- } elseif ($ mode === self ::USE_KEY ) {
103- $ keyVar = $ callbackArg ->params [0 ]->var ;
104- $ itemVar = null ;
105- } elseif ($ mode === self ::USE_BOTH ) {
106- $ keyVar = $ callbackArg ->params [1 ]->var ?? null ;
107- $ itemVar = $ callbackArg ->params [0 ]->var ;
108- }
109- return $ this ->filterByTruthyValue ($ scope , $ itemVar , $ arrayArgType , $ keyVar , $ statement ->expr );
110- }
111- } elseif ($ callbackArg instanceof ArrowFunction && count ($ callbackArg ->params ) > 0 ) {
112- if ($ mode === self ::USE_ITEM ) {
113- $ keyVar = null ;
114- $ itemVar = $ callbackArg ->params [0 ]->var ;
115- } elseif ($ mode === self ::USE_KEY ) {
116- $ keyVar = $ callbackArg ->params [0 ]->var ;
117- $ itemVar = null ;
118- } elseif ($ mode === self ::USE_BOTH ) {
119- $ keyVar = $ callbackArg ->params [1 ]->var ?? null ;
120- $ itemVar = $ callbackArg ->params [0 ]->var ;
121- }
122- return $ this ->filterByTruthyValue ($ scope , $ itemVar , $ arrayArgType , $ keyVar , $ callbackArg ->expr );
123- } elseif (
124- ($ callbackArg instanceof FuncCall || $ callbackArg instanceof MethodCall || $ callbackArg instanceof StaticCall)
125- && $ callbackArg ->isFirstClassCallable ()
126- ) {
127- [$ args , $ itemVar , $ keyVar ] = $ this ->createDummyArgs ($ mode );
128- $ expr = clone $ callbackArg ;
129- $ expr ->args = $ args ;
130- return $ this ->filterByTruthyValue ($ scope , $ itemVar , $ arrayArgType , $ keyVar , $ expr );
131- } else {
132- $ constantStrings = $ scope ->getType ($ callbackArg )->getConstantStrings ();
133- if (count ($ constantStrings ) > 0 ) {
134- $ results = [];
135- [$ args , $ itemVar , $ keyVar ] = $ this ->createDummyArgs ($ mode );
136-
137- foreach ($ constantStrings as $ constantString ) {
138- $ funcName = self ::createFunctionName ($ constantString ->getValue ());
139- if ($ funcName === null ) {
140- $ results [] = new ErrorType ();
141- continue ;
142- }
143-
144- $ expr = new FuncCall ($ funcName , $ args );
145- $ results [] = $ this ->filterByTruthyValue ($ scope , $ itemVar , $ arrayArgType , $ keyVar , $ expr );
146- }
147- return TypeCombinator::union (...$ results );
148- }
149- }
150-
151- return new ArrayType ($ keyType , $ itemType );
152- }
153-
154- public function removeFalsey (Type $ type ): Type
155- {
156- $ falseyTypes = StaticTypeFactory::falsey ();
157-
158- if (count ($ type ->getConstantArrays ()) > 0 ) {
159- $ result = [];
160- foreach ($ type ->getConstantArrays () as $ constantArray ) {
161- $ keys = $ constantArray ->getKeyTypes ();
162- $ values = $ constantArray ->getValueTypes ();
163-
164- $ builder = ConstantArrayTypeBuilder::createEmpty ();
165-
166- foreach ($ values as $ offset => $ value ) {
167- $ isFalsey = $ falseyTypes ->isSuperTypeOf ($ value );
168-
169- if ($ isFalsey ->maybe ()) {
170- $ builder ->setOffsetValueType ($ keys [$ offset ], TypeCombinator::remove ($ value , $ falseyTypes ), true );
171- } elseif ($ isFalsey ->no ()) {
172- $ builder ->setOffsetValueType ($ keys [$ offset ], $ value , $ constantArray ->isOptionalKey ($ offset ));
173- }
174- }
175-
176- $ result [] = $ builder ->getArray ();
177- }
178-
179- return TypeCombinator::union (...$ result );
180- }
181-
182- $ keyType = $ type ->getIterableKeyType ();
183- $ valueType = $ type ->getIterableValueType ();
184-
185- $ valueType = TypeCombinator::remove ($ valueType , $ falseyTypes );
186-
187- if ($ valueType instanceof NeverType) {
188- return new ConstantArrayType ([], []);
189- }
190-
191- return new ArrayType ($ keyType , $ valueType );
192- }
193-
194- private function filterByTruthyValue (Scope $ scope , Error |Variable |null $ itemVar , Type $ arrayType , Error |Variable |null $ keyVar , Expr $ expr ): Type
195- {
196- if (!$ scope instanceof MutatingScope) {
197- throw new ShouldNotHappenException ();
198- }
199-
200- $ constantArrays = $ arrayType ->getConstantArrays ();
201- if (count ($ constantArrays ) > 0 ) {
202- $ results = [];
203- foreach ($ constantArrays as $ constantArray ) {
204- $ builder = ConstantArrayTypeBuilder::createEmpty ();
205- $ optionalKeys = $ constantArray ->getOptionalKeys ();
206- foreach ($ constantArray ->getKeyTypes () as $ i => $ keyType ) {
207- $ itemType = $ constantArray ->getValueTypes ()[$ i ];
208- [$ newKeyType , $ newItemType , $ optional ] = $ this ->processKeyAndItemType ($ scope , $ keyType , $ itemType , $ itemVar , $ keyVar , $ expr );
209- $ optional = $ optional || in_array ($ i , $ optionalKeys , true );
210- if ($ newKeyType instanceof NeverType || $ newItemType instanceof NeverType) {
211- continue ;
212- }
213- if ($ itemType ->equals ($ newItemType ) && $ keyType ->equals ($ newKeyType )) {
214- $ builder ->setOffsetValueType ($ keyType , $ itemType , $ optional );
215- continue ;
216- }
217-
218- $ builder ->setOffsetValueType ($ newKeyType , $ newItemType , true );
219- }
220-
221- $ results [] = $ builder ->getArray ();
222- }
223-
224- return TypeCombinator::union (...$ results );
225- }
226-
227- [$ newKeyType , $ newItemType ] = $ this ->processKeyAndItemType ($ scope , $ arrayType ->getIterableKeyType (), $ arrayType ->getIterableValueType (), $ itemVar , $ keyVar , $ expr );
228-
229- if ($ newItemType instanceof NeverType || $ newKeyType instanceof NeverType) {
230- return new ConstantArrayType ([], []);
231- }
232-
233- return new ArrayType ($ newKeyType , $ newItemType );
234- }
235-
236- /**
237- * @return array{Type, Type, bool}
238- */
239- private function processKeyAndItemType (MutatingScope $ scope , Type $ keyType , Type $ itemType , Error |Variable |null $ itemVar , Error |Variable |null $ keyVar , Expr $ expr ): array
240- {
241- $ itemVarName = null ;
242- if ($ itemVar !== null ) {
243- if (!$ itemVar instanceof Variable || !is_string ($ itemVar ->name )) {
244- throw new ShouldNotHappenException ();
245- }
246- $ itemVarName = $ itemVar ->name ;
247- $ scope = $ scope ->assignVariable ($ itemVarName , $ itemType , new MixedType ());
248- }
249-
250- $ keyVarName = null ;
251- if ($ keyVar !== null ) {
252- if (!$ keyVar instanceof Variable || !is_string ($ keyVar ->name )) {
253- throw new ShouldNotHappenException ();
254- }
255- $ keyVarName = $ keyVar ->name ;
256- $ scope = $ scope ->assignVariable ($ keyVarName , $ keyType , new MixedType ());
257- }
258-
259- $ booleanResult = $ scope ->getType ($ expr )->toBoolean ();
260- if ($ booleanResult ->isFalse ()->yes ()) {
261- return [new NeverType (), new NeverType (), false ];
262- }
263-
264- $ scope = $ scope ->filterByTruthyValue ($ expr );
265-
266- return [
267- $ keyVarName !== null ? $ scope ->getVariableType ($ keyVarName ) : $ keyType ,
268- $ itemVarName !== null ? $ scope ->getVariableType ($ itemVarName ) : $ itemType ,
269- !$ booleanResult instanceof ConstantBooleanType,
270- ];
271- }
272-
273- private static function createFunctionName (string $ funcName ): ?Name
274- {
275- if ($ funcName === '' ) {
276- return null ;
277- }
278-
279- if ($ funcName [0 ] === '\\' ) {
280- $ funcName = substr ($ funcName , 1 );
281-
282- if ($ funcName === '' ) {
283- return null ;
284- }
285-
286- return new Name \FullyQualified ($ funcName );
287- }
288-
289- return new Name ($ funcName );
290- }
291-
292- /**
293- * @param self::USE_* $mode
294- * @return array{list<Arg>, ?Variable, ?Variable}
295- */
296- private function createDummyArgs (int $ mode ): array
297- {
298- if ($ mode === self ::USE_ITEM ) {
299- $ itemVar = new Variable ('item ' );
300- $ keyVar = null ;
301- $ args = [new Arg ($ itemVar )];
302- } elseif ($ mode === self ::USE_KEY ) {
303- $ itemVar = null ;
304- $ keyVar = new Variable ('key ' );
305- $ args = [new Arg ($ keyVar )];
306- } elseif ($ mode === self ::USE_BOTH ) {
307- $ itemVar = new Variable ('item ' );
308- $ keyVar = new Variable ('key ' );
309- $ args = [new Arg ($ itemVar ), new Arg ($ keyVar )];
310- }
311- return [$ args , $ itemVar , $ keyVar ];
312- }
313-
314- /**
315- * @param non-empty-string $constantName
316- */
317- private function getConstant (string $ constantName ): int
318- {
319- $ constant = $ this ->reflectionProvider ->getConstant (new Name ($ constantName ), null );
320- $ valueType = $ constant ->getValueType ();
321- if (!$ valueType instanceof ConstantIntegerType) {
322- throw new ShouldNotHappenException (sprintf ('Constant %s does not have integer type. ' , $ constantName ));
323- }
324-
325- return $ valueType ->getValue ();
326- }
327-
328- /**
329- * @return self::USE_*|null
330- */
331- private function determineMode (?Expr $ flagArg , Scope $ scope ): ?int
332- {
333- if ($ flagArg === null ) {
334- return self ::USE_ITEM ;
335- }
336-
337- $ flagValues = $ scope ->getType ($ flagArg )->getConstantScalarValues ();
338- if (count ($ flagValues ) !== 1 ) {
339- return null ;
340- }
341-
342- if ($ flagValues [0 ] === $ this ->getConstant ('ARRAY_FILTER_USE_KEY ' )) {
343- return self ::USE_KEY ;
344- } elseif ($ flagValues [0 ] === $ this ->getConstant ('ARRAY_FILTER_USE_BOTH ' )) {
345- return self ::USE_BOTH ;
346- }
347-
348- return null ;
29+ return $ this ->arrayFilterFunctionReturnTypeHelper ->getType ($ scope , $ arrayArg , $ callbackArg , $ flagArg );
34930 }
35031
35132}
0 commit comments