ArrayFunctionArgumentsAnalyzer::handleAddition()   F
last analyzed

Complexity

Conditions 21
Paths 19

Size

Total Lines 165

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
nc 19
nop 4
dl 0
loc 165
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
6
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\ArrayAssignmentAnalyzer;
7
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
8
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
9
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
10
use Psalm\Internal\Analyzer\StatementsAnalyzer;
11
use Psalm\Internal\Analyzer\TypeAnalyzer;
12
use Psalm\Internal\Codebase\InternalCallMapHandler;
13
use Psalm\Internal\Type\TypeCombination;
14
use Psalm\Internal\Type\UnionTemplateHandler;
15
use Psalm\CodeLocation;
16
use Psalm\Context;
17
use Psalm\Issue\InvalidArgument;
18
use Psalm\Issue\InvalidScalarArgument;
19
use Psalm\Issue\MixedArgumentTypeCoercion;
20
use Psalm\Issue\PossiblyInvalidArgument;
21
use Psalm\Issue\TooFewArguments;
22
use Psalm\Issue\TooManyArguments;
23
use Psalm\Issue\ArgumentTypeCoercion;
24
use Psalm\IssueBuffer;
25
use Psalm\Type;
26
use Psalm\Type\Atomic\ObjectLike;
27
use Psalm\Type\Atomic\TArray;
28
use Psalm\Type\Atomic\TEmpty;
29
use Psalm\Type\Atomic\TList;
30
use Psalm\Type\Atomic\TNonEmptyArray;
31
use Psalm\Type\Atomic\TNonEmptyList;
32
use function strtolower;
33
use function strpos;
34
use function explode;
35
use function count;
36
use function array_filter;
37
use function assert;
38
39
/**
40
 * @internal
41
 */
42
class ArrayFunctionArgumentsAnalyzer
43
{
44
    /**
45
     * @param   StatementsAnalyzer              $statements_analyzer
46
     * @param   array<int, PhpParser\Node\Arg> $args
47
     * @param   string                         $method_id
48
     *
49
     * @return  false|null
50
     */
51
    public static function checkArgumentsMatch(
52
        StatementsAnalyzer $statements_analyzer,
53
        Context $context,
54
        array $args,
55
        $method_id,
56
        bool $check_functions
57
    ) {
58
        $closure_index = $method_id === 'array_map' ? 0 : 1;
59
60
        $array_arg_types = [];
61
62
        foreach ($args as $i => $arg) {
63
            if ($i === 0 && $method_id === 'array_map') {
64
                continue;
65
            }
66
67
            if ($i === 1 && $method_id === 'array_filter') {
68
                break;
69
            }
70
71
            /**
72
             * @psalm-suppress PossiblyUndefinedStringArrayOffset
73
             * @var ObjectLike|TArray|TList|null
74
             */
75
            $array_arg_type = ($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
76
                    && ($types = $arg_value_type->getAtomicTypes())
77
                    && isset($types['array'])
78
                ? $types['array']
79
                : null;
80
81
            if ($array_arg_type instanceof ObjectLike) {
82
                $array_arg_type = $array_arg_type->getGenericArrayType();
83
            }
84
85
            if ($array_arg_type instanceof TList) {
86
                $array_arg_type = new TArray([Type::getInt(), $array_arg_type->type_param]);
87
            }
88
89
            $array_arg_types[] = $array_arg_type;
90
        }
91
92
        $closure_arg = isset($args[$closure_index]) ? $args[$closure_index] : null;
93
94
        $closure_arg_type = null;
95
96
        if ($closure_arg) {
97
            $closure_arg_type = $statements_analyzer->node_data->getType($closure_arg->value);
98
        }
99
100
        if ($closure_arg && $closure_arg_type) {
101
            $min_closure_param_count = $max_closure_param_count = count($array_arg_types);
102
103
            if ($method_id === 'array_filter') {
104
                $max_closure_param_count = count($args) > 2 ? 2 : 1;
105
            }
106
107
            foreach ($closure_arg_type->getAtomicTypes() as $closure_type) {
108
                self::checkClosureType(
109
                    $statements_analyzer,
110
                    $context,
111
                    $method_id,
112
                    $closure_type,
113
                    $closure_arg,
114
                    $min_closure_param_count,
115
                    $max_closure_param_count,
116
                    $array_arg_types,
117
                    $check_functions
118
                );
119
            }
120
        }
121
    }
122
123
    /**
124
     * @param   StatementsAnalyzer                      $statements_analyzer
125
     * @param   array<int, PhpParser\Node\Arg>          $args
126
     * @param   Context                                 $context
127
     *
128
     * @return  false|null
129
     */
130
    public static function handleAddition(
131
        StatementsAnalyzer $statements_analyzer,
132
        array $args,
133
        Context $context,
134
        bool $is_push
135
    ) {
136
        $array_arg = $args[0]->value;
137
138
        $unpacked_args = array_filter(
139
            $args,
140
            function ($arg) {
141
                return $arg->unpack;
142
            }
143
        );
144
145
        if ($is_push && !$unpacked_args) {
146
            for ($i = 1; $i < count($args); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
147
                $was_inside_assignment = $context->inside_assignment;
148
149
                $context->inside_assignment = true;
150
151
                if (ExpressionAnalyzer::analyze(
152
                    $statements_analyzer,
153
                    $args[$i]->value,
154
                    $context
155
                ) === false) {
156
                    return false;
157
                }
158
159
                $context->inside_assignment = $was_inside_assignment;
160
161
                $old_node_data = $statements_analyzer->node_data;
162
163
                $statements_analyzer->node_data = clone $statements_analyzer->node_data;
164
165
                ArrayAssignmentAnalyzer::analyze(
166
                    $statements_analyzer,
167
                    new PhpParser\Node\Expr\ArrayDimFetch(
168
                        $args[0]->value,
169
                        null,
170
                        $args[$i]->value->getAttributes()
171
                    ),
172
                    $context,
173
                    $args[$i]->value,
174
                    $statements_analyzer->node_data->getType($args[$i]->value) ?: Type::getMixed()
175
                );
176
177
                $statements_analyzer->node_data = $old_node_data;
178
            }
179
180
            return;
181
        }
182
183
        $context->inside_call = true;
184
185
        if (ExpressionAnalyzer::analyze(
186
            $statements_analyzer,
187
            $array_arg,
188
            $context
189
        ) === false) {
190
            return false;
191
        }
192
193
        for ($i = 1; $i < count($args); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
194
            if (ExpressionAnalyzer::analyze(
195
                $statements_analyzer,
196
                $args[$i]->value,
197
                $context
198
            ) === false) {
199
                return false;
200
            }
201
        }
202
203
        if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
204
            && $array_arg_type->hasArray()
205
        ) {
206
            /**
207
             * @psalm-suppress PossiblyUndefinedStringArrayOffset
208
             * @var TArray|ObjectLike|TList
209
             */
210
            $array_type = $array_arg_type->getAtomicTypes()['array'];
211
212
            $objectlike_list = null;
213
214
            if ($array_type instanceof ObjectLike) {
215
                if ($array_type->is_list) {
216
                    $objectlike_list = clone $array_type;
217
                }
218
219
                $array_type = $array_type->getGenericArrayType();
220
            }
221
222
            $by_ref_type = new Type\Union([clone $array_type]);
223
224
            foreach ($args as $argument_offset => $arg) {
225
                if ($argument_offset === 0) {
226
                    continue;
227
                }
228
229
                if (ExpressionAnalyzer::analyze(
230
                    $statements_analyzer,
231
                    $arg->value,
232
                    $context
233
                ) === false) {
234
                    return false;
235
                }
236
237
                if (!($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
238
                    || $arg_value_type->hasMixed()
239
                ) {
240
                    $by_ref_type = Type::combineUnionTypes(
241
                        $by_ref_type,
242
                        new Type\Union([new TArray([Type::getInt(), Type::getMixed()])])
243
                    );
244
                } elseif ($arg->unpack) {
245
                    $by_ref_type = Type::combineUnionTypes(
246
                        $by_ref_type,
247
                        clone $arg_value_type
248
                    );
249
                } else {
250
                    if ($objectlike_list) {
251
                        \array_unshift($objectlike_list->properties, $arg_value_type);
252
253
                        $by_ref_type = new Type\Union([$objectlike_list]);
254
                    } elseif ($array_type instanceof TList) {
255
                        $by_ref_type = Type::combineUnionTypes(
256
                            $by_ref_type,
257
                            new Type\Union(
258
                                [
259
                                    new TNonEmptyList(clone $arg_value_type),
260
                                ]
261
                            )
262
                        );
263
                    } else {
264
                        $by_ref_type = Type::combineUnionTypes(
265
                            $by_ref_type,
266
                            new Type\Union(
267
                                [
268
                                    new TNonEmptyArray(
269
                                        [
270
                                            Type::getInt(),
271
                                            clone $arg_value_type
272
                                        ]
273
                                    ),
274
                                ]
275
                            )
276
                        );
277
                    }
278
                }
279
            }
280
281
            AssignmentAnalyzer::assignByRefParam(
282
                $statements_analyzer,
283
                $array_arg,
284
                $by_ref_type,
285
                $by_ref_type,
286
                $context,
287
                false
288
            );
289
        }
290
291
        $context->inside_call = false;
292
293
        return;
294
    }
295
296
    /**
297
     * @param   StatementsAnalyzer                      $statements_analyzer
298
     * @param   array<int, PhpParser\Node\Arg>          $args
299
     * @param   Context                                 $context
300
     *
301
     * @return  false|null
302
     */
303
    public static function handleSplice(
304
        StatementsAnalyzer $statements_analyzer,
305
        array $args,
306
        Context $context
307
    ) {
308
        $context->inside_call = true;
309
        $array_arg = $args[0]->value;
310
311
        if (ExpressionAnalyzer::analyze(
312
            $statements_analyzer,
313
            $array_arg,
314
            $context
315
        ) === false) {
316
            return false;
317
        }
318
319
        $offset_arg = $args[1]->value;
320
321
        if (ExpressionAnalyzer::analyze(
322
            $statements_analyzer,
323
            $offset_arg,
324
            $context
325
        ) === false) {
326
            return false;
327
        }
328
329
        if (!isset($args[2])) {
330
            return;
331
        }
332
333
        $length_arg = $args[2]->value;
334
335
        if (ExpressionAnalyzer::analyze(
336
            $statements_analyzer,
337
            $length_arg,
338
            $context
339
        ) === false) {
340
            return false;
341
        }
342
343
        if (!isset($args[3])) {
344
            return;
345
        }
346
347
        $replacement_arg = $args[3]->value;
348
349
        if (ExpressionAnalyzer::analyze(
350
            $statements_analyzer,
351
            $replacement_arg,
352
            $context
353
        ) === false) {
354
            return false;
355
        }
356
357
        $context->inside_call = false;
358
359
        $replacement_arg_type = $statements_analyzer->node_data->getType($replacement_arg);
360
361
        if ($replacement_arg_type
362
            && !$replacement_arg_type->hasArray()
363
            && $replacement_arg_type->hasString()
364
            && $replacement_arg_type->isSingle()
365
        ) {
366
            $replacement_arg_type = new Type\Union([
367
                new Type\Atomic\TArray([Type::getInt(), $replacement_arg_type])
368
            ]);
369
370
            $statements_analyzer->node_data->setType($replacement_arg, $replacement_arg_type);
371
        }
372
373
        if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
374
            && $array_arg_type->hasArray()
375
            && $replacement_arg_type
376
            && $replacement_arg_type->hasArray()
377
        ) {
378
            /**
379
             * @psalm-suppress PossiblyUndefinedStringArrayOffset
380
             * @var TArray|ObjectLike|TList
381
             */
382
            $array_type = $array_arg_type->getAtomicTypes()['array'];
383
384
            if ($array_type instanceof ObjectLike) {
385
                if ($array_type->is_list) {
386
                    $array_type = new TNonEmptyList($array_type->getGenericValueType());
387
                } else {
388
                    $array_type = $array_type->getGenericArrayType();
389
                }
390
            }
391
392
            if ($array_type instanceof TArray
393
                && $array_type->type_params[0]->hasInt()
394
                && !$array_type->type_params[0]->hasString()
395
            ) {
396
                if ($array_type instanceof TNonEmptyArray) {
397
                    $array_type = new TNonEmptyList($array_type->type_params[1]);
398
                } else {
399
                    $array_type = new TList($array_type->type_params[1]);
400
                }
401
            }
402
403
            /**
404
             * @psalm-suppress PossiblyUndefinedStringArrayOffset
405
             * @var TArray|ObjectLike|TList
406
             */
407
            $replacement_array_type = $replacement_arg_type->getAtomicTypes()['array'];
408
409
            $by_ref_type = TypeCombination::combineTypes([$array_type, $replacement_array_type]);
410
411
            AssignmentAnalyzer::assignByRefParam(
412
                $statements_analyzer,
413
                $array_arg,
414
                $by_ref_type,
415
                $by_ref_type,
416
                $context,
417
                false
418
            );
419
420
            return;
421
        }
422
423
        $array_type = Type::getArray();
424
425
        AssignmentAnalyzer::assignByRefParam(
426
            $statements_analyzer,
427
            $array_arg,
428
            $array_type,
429
            $array_type,
430
            $context,
431
            false
432
        );
433
    }
434
435
    /**
436
     * @return void
437
     */
438
    public static function handleByRefArrayAdjustment(
439
        StatementsAnalyzer $statements_analyzer,
440
        PhpParser\Node\Arg $arg,
441
        Context $context
442
    ) {
443
        $var_id = ExpressionIdentifier::getVarId(
444
            $arg->value,
445
            $statements_analyzer->getFQCLN(),
446
            $statements_analyzer
447
        );
448
449
        if ($var_id) {
450
            $context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
451
452
            if (isset($context->vars_in_scope[$var_id])) {
453
                $array_type = clone $context->vars_in_scope[$var_id];
454
455
                $array_atomic_types = $array_type->getAtomicTypes();
456
457
                foreach ($array_atomic_types as $array_atomic_type) {
458
                    if ($array_atomic_type instanceof ObjectLike) {
459
                        $array_atomic_type = $array_atomic_type->getGenericArrayType();
460
                    }
461
462
                    if ($array_atomic_type instanceof TNonEmptyArray) {
463 View Code Duplication
                        if (!$context->inside_loop && $array_atomic_type->count !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
464
                            if ($array_atomic_type->count === 0) {
465
                                $array_atomic_type = new TArray(
466
                                    [
467
                                        new Type\Union([new TEmpty]),
468
                                        new Type\Union([new TEmpty]),
469
                                    ]
470
                                );
471
                            } else {
472
                                $array_atomic_type->count--;
473
                            }
474
                        } else {
475
                            $array_atomic_type = new TArray($array_atomic_type->type_params);
476
                        }
477
478
                        $array_type->addType($array_atomic_type);
479
                        $context->removeDescendents($var_id, $array_type);
480
                    } elseif ($array_atomic_type instanceof TNonEmptyList) {
481 View Code Duplication
                        if (!$context->inside_loop && $array_atomic_type->count !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
482
                            if ($array_atomic_type->count === 0) {
483
                                $array_atomic_type = new TArray(
484
                                    [
485
                                        new Type\Union([new TEmpty]),
486
                                        new Type\Union([new TEmpty]),
487
                                    ]
488
                                );
489
                            } else {
490
                                $array_atomic_type->count--;
491
                            }
492
                        } else {
493
                            $array_atomic_type = new TList($array_atomic_type->type_param);
494
                        }
495
496
                        $array_type->addType($array_atomic_type);
497
                        $context->removeDescendents($var_id, $array_type);
498
                    }
499
                }
500
501
                $context->vars_in_scope[$var_id] = $array_type;
502
            }
503
        }
504
    }
505
506
    /**
507
     * @param  string   $method_id
508
     * @param  int      $min_closure_param_count
509
     * @param  int      $max_closure_param_count [description]
510
     * @param  (TArray|null)[] $array_arg_types
511
     *
512
     * @return void
513
     */
514
    private static function checkClosureType(
515
        StatementsAnalyzer $statements_analyzer,
516
        Context $context,
517
        $method_id,
518
        Type\Atomic $closure_type,
519
        PhpParser\Node\Arg $closure_arg,
520
        $min_closure_param_count,
521
        $max_closure_param_count,
522
        array $array_arg_types,
523
        bool $check_functions
524
    ) {
525
        $codebase = $statements_analyzer->getCodebase();
526
527
        if (!$closure_type instanceof Type\Atomic\TFn) {
528
            if ($method_id === 'array_map') {
529
                return;
530
            }
531
532
            if (!$closure_arg->value instanceof PhpParser\Node\Scalar\String_
533
                && !$closure_arg->value instanceof PhpParser\Node\Expr\Array_
534
                && !$closure_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat
535
            ) {
536
                return;
537
            }
538
539
            $function_ids = CallAnalyzer::getFunctionIdsFromCallableArg(
540
                $statements_analyzer,
541
                $closure_arg->value
542
            );
543
544
            $closure_types = [];
545
546
            foreach ($function_ids as $function_id) {
547
                $function_id = strtolower($function_id);
548
549
                if (strpos($function_id, '::') !== false) {
550
                    if ($function_id[0] === '$') {
551
                        $function_id = \substr($function_id, 1);
552
                    }
553
554
                    $function_id_parts = explode('&', $function_id);
555
556
                    foreach ($function_id_parts as $function_id_part) {
557
                        list($callable_fq_class_name, $method_name) = explode('::', $function_id_part);
558
559
                        switch ($callable_fq_class_name) {
560
                            case 'self':
561
                            case 'static':
562
                            case 'parent':
563
                                $container_class = $statements_analyzer->getFQCLN();
564
565
                                if ($callable_fq_class_name === 'parent') {
566
                                    $container_class = $statements_analyzer->getParentFQCLN();
567
                                }
568
569
                                if (!$container_class) {
570
                                    continue 2;
571
                                }
572
573
                                $callable_fq_class_name = $container_class;
574
                        }
575
576
                        if (!$codebase->classOrInterfaceExists($callable_fq_class_name)) {
577
                            return;
578
                        }
579
580
                        $function_id_part = new \Psalm\Internal\MethodIdentifier(
581
                            $callable_fq_class_name,
582
                            strtolower($method_name)
583
                        );
584
585
                        try {
586
                            $method_storage = $codebase->methods->getStorage($function_id_part);
587
                        } catch (\UnexpectedValueException $e) {
588
                            // the method may not exist, but we're suppressing that issue
589
                            continue;
590
                        }
591
592
                        $closure_types[] = new Type\Atomic\TFn(
593
                            'Closure',
594
                            $method_storage->params,
595
                            $method_storage->return_type ?: Type::getMixed()
596
                        );
597
                    }
598
                } else {
599
                    if (!$check_functions) {
600
                        continue;
601
                    }
602
603
                    if (!$codebase->functions->functionExists($statements_analyzer, $function_id)) {
604
                        continue;
605
                    }
606
607
                    $function_storage = $codebase->functions->getStorage(
608
                        $statements_analyzer,
609
                        $function_id
610
                    );
611
612
                    if (InternalCallMapHandler::inCallMap($function_id)) {
613
                        $callmap_callables = InternalCallMapHandler::getCallablesFromCallMap($function_id);
614
615
                        if ($callmap_callables === null) {
616
                            throw new \UnexpectedValueException('This should not happen');
617
                        }
618
619
                        $passing_callmap_callables = [];
620
621
                        foreach ($callmap_callables as $callmap_callable) {
622
                            $required_param_count = 0;
623
624
                            assert($callmap_callable->params !== null);
625
626 View Code Duplication
                            foreach ($callmap_callable->params as $i => $param) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
627
                                if (!$param->is_optional && !$param->is_variadic) {
628
                                    $required_param_count = $i + 1;
629
                                }
630
                            }
631
632
                            if ($required_param_count <= $max_closure_param_count) {
633
                                $passing_callmap_callables[] = $callmap_callable;
634
                            }
635
                        }
636
637
                        if ($passing_callmap_callables) {
638
                            foreach ($passing_callmap_callables as $passing_callmap_callable) {
639
                                $closure_types[] = $passing_callmap_callable;
640
                            }
641
                        } else {
642
                            $closure_types[] = $callmap_callables[0];
643
                        }
644
                    } else {
645
                        $closure_types[] = new Type\Atomic\TFn(
646
                            'Closure',
647
                            $function_storage->params,
648
                            $function_storage->return_type ?: Type::getMixed()
649
                        );
650
                    }
651
                }
652
            }
653
        } else {
654
            $closure_types = [$closure_type];
655
        }
656
657
        foreach ($closure_types as $closure_type) {
658
            if ($closure_type->params === null) {
659
                continue;
660
            }
661
662
            self::checkClosureTypeArgs(
663
                $statements_analyzer,
664
                $context,
665
                $method_id,
666
                $closure_type,
667
                $closure_arg,
668
                $min_closure_param_count,
669
                $max_closure_param_count,
670
                $array_arg_types
671
            );
672
        }
673
    }
674
675
    /**
676
     * @param  Type\Atomic\TFn|Type\Atomic\TCallable $closure_type
677
     * @param  string   $method_id
678
     * @param  int      $min_closure_param_count
679
     * @param  int      $max_closure_param_count
680
     * @param  (TArray|null)[] $array_arg_types
681
     *
682
     * @return void
683
     */
684
    private static function checkClosureTypeArgs(
685
        StatementsAnalyzer $statements_analyzer,
686
        Context $context,
687
        $method_id,
688
        Type\Atomic $closure_type,
689
        PhpParser\Node\Arg $closure_arg,
690
        $min_closure_param_count,
691
        $max_closure_param_count,
692
        array $array_arg_types
693
    ) {
694
        $codebase = $statements_analyzer->getCodebase();
695
696
        $closure_params = $closure_type->params;
0 ignored issues
show
Bug introduced by
The property params does not seem to exist in Psalm\Type\Atomic.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
697
698
        if ($closure_params === null) {
699
            throw new \UnexpectedValueException('Closure params should not be null here');
700
        }
701
702
        $required_param_count = 0;
703
704
        foreach ($closure_params as $i => $param) {
705
            if (!$param->is_optional && !$param->is_variadic) {
706
                $required_param_count = $i + 1;
707
            }
708
        }
709
710
        if (count($closure_params) < $min_closure_param_count) {
711
            $argument_text = $min_closure_param_count === 1 ? 'one argument' : $min_closure_param_count . ' arguments';
712
713
            if (IssueBuffer::accepts(
714
                new TooManyArguments(
715
                    'The callable passed to ' . $method_id . ' will be called with ' . $argument_text . ', expecting '
716
                        . $required_param_count,
717
                    new CodeLocation($statements_analyzer->getSource(), $closure_arg),
718
                    $method_id
719
                ),
720
                $statements_analyzer->getSuppressedIssues()
721
            )) {
722
                // fall through
723
            }
724
725
            return;
726
        } elseif ($required_param_count > $max_closure_param_count) {
727
            $argument_text = $max_closure_param_count === 1 ? 'one argument' : $max_closure_param_count . ' arguments';
728
729
            if (IssueBuffer::accepts(
730
                new TooFewArguments(
731
                    'The callable passed to ' . $method_id . ' will be called with ' . $argument_text . ', expecting '
732
                        . $required_param_count,
733
                    new CodeLocation($statements_analyzer->getSource(), $closure_arg),
734
                    $method_id
735
                ),
736
                $statements_analyzer->getSuppressedIssues()
737
            )) {
738
                // fall through
739
            }
740
741
            return;
742
        }
743
744
        // abandon attempt to validate closure params if we have an extra arg for ARRAY_FILTER
745
        if ($method_id === 'array_filter' && $max_closure_param_count > 1) {
746
            return;
747
        }
748
749
        foreach ($closure_params as $i => $closure_param) {
750
            if (!isset($array_arg_types[$i])) {
751
                continue;
752
            }
753
754
            $array_arg_type = $array_arg_types[$i];
755
756
            $input_type = $array_arg_type->type_params[1];
757
758
            if ($input_type->hasMixed()) {
759
                continue;
760
            }
761
762
            $closure_param_type = $closure_param->type;
763
764
            if (!$closure_param_type) {
765
                continue;
766
            }
767
768
            if ($method_id === 'array_map'
769
                && $i === 0
770
                && $closure_type->return_type
771
                && $closure_param_type->hasTemplate()
772
            ) {
773
                $closure_param_type = clone $closure_param_type;
774
                $closure_type->return_type = clone $closure_type->return_type;
0 ignored issues
show
Bug introduced by
The property return_type does not seem to exist in Psalm\Type\Atomic.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
775
776
                $template_result = new \Psalm\Internal\Type\TemplateResult(
777
                    [],
778
                    []
779
                );
780
781
                foreach ($closure_param_type->getTemplateTypes() as $template_type) {
782
                    $template_result->template_types[$template_type->param_name] = [
783
                        ($template_type->defining_class) => [$template_type->as]
784
                    ];
785
                }
786
787
                $closure_param_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
788
                    $closure_param_type,
789
                    $template_result,
790
                    $codebase,
791
                    $statements_analyzer,
792
                    $input_type,
793
                    $i,
794
                    $context->self,
795
                    $context->calling_method_id ?: $context->calling_function_id
796
                );
797
798
                $closure_type->return_type->replaceTemplateTypesWithArgTypes(
799
                    $template_result,
800
                    $codebase
801
                );
802
            }
803
804
            $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
805
806
            $type_match_found = TypeAnalyzer::isContainedBy(
807
                $codebase,
808
                $input_type,
809
                $closure_param_type,
810
                $input_type->ignore_nullable_issues,
811
                $input_type->ignore_falsable_issues,
812
                $union_comparison_results
813
            );
814
815
            if ($union_comparison_results->type_coerced) {
816
                if ($union_comparison_results->type_coerced_from_mixed) {
817
                    if (IssueBuffer::accepts(
818
                        new MixedArgumentTypeCoercion(
819
                            'First parameter of closure passed to function ' . $method_id . ' expects ' .
820
                                $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided',
821
                            new CodeLocation($statements_analyzer->getSource(), $closure_arg),
822
                            $method_id
823
                        ),
824
                        $statements_analyzer->getSuppressedIssues()
825
                    )) {
826
                        // keep soldiering on
827
                    }
828
                } else {
829
                    if (IssueBuffer::accepts(
830
                        new ArgumentTypeCoercion(
831
                            'First parameter of closure passed to function ' . $method_id . ' expects ' .
832
                                $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided',
833
                            new CodeLocation($statements_analyzer->getSource(), $closure_arg),
834
                            $method_id
835
                        ),
836
                        $statements_analyzer->getSuppressedIssues()
837
                    )) {
838
                        // keep soldiering on
839
                    }
840
                }
841
            }
842
843
            if (!$union_comparison_results->type_coerced && !$type_match_found) {
844
                $types_can_be_identical = TypeAnalyzer::canExpressionTypesBeIdentical(
845
                    $codebase,
846
                    $input_type,
847
                    $closure_param_type
848
                );
849
850
                if ($union_comparison_results->scalar_type_match_found) {
851
                    if (IssueBuffer::accepts(
852
                        new InvalidScalarArgument(
853
                            'First parameter of closure passed to function ' . $method_id . ' expects ' .
854
                                $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided',
855
                            new CodeLocation($statements_analyzer->getSource(), $closure_arg),
856
                            $method_id
857
                        ),
858
                        $statements_analyzer->getSuppressedIssues()
859
                    )) {
860
                        // fall through
861
                    }
862
                } elseif ($types_can_be_identical) {
863
                    if (IssueBuffer::accepts(
864
                        new PossiblyInvalidArgument(
865
                            'First parameter of closure passed to function ' . $method_id . ' expects '
866
                                . $closure_param_type->getId() . ', possibly different type '
867
                                . $input_type->getId() . ' provided',
868
                            new CodeLocation($statements_analyzer->getSource(), $closure_arg),
869
                            $method_id
870
                        ),
871
                        $statements_analyzer->getSuppressedIssues()
872
                    )) {
873
                        // fall through
874
                    }
875
                } elseif (IssueBuffer::accepts(
876
                    new InvalidArgument(
877
                        'First parameter of closure passed to function ' . $method_id . ' expects ' .
878
                            $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided',
879
                        new CodeLocation($statements_analyzer->getSource(), $closure_arg),
880
                        $method_id
881
                    ),
882
                    $statements_analyzer->getSuppressedIssues()
883
                )) {
884
                    // fall through
885
                }
886
            }
887
        }
888
    }
889
}
890