Test Setup Failed
Push — master ( d1ca68...38fdf4 )
by Matthew
04:54
created

ArrayFunctionArgumentsAnalyzer::handleAddition()   F

Complexity

Conditions 21
Paths 19

Size

Total Lines 156

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
nc 19
nop 4
dl 0
loc 156
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
        if ($is_push) {
139
            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...
140
                if (ExpressionAnalyzer::analyze(
141
                    $statements_analyzer,
142
                    $args[$i]->value,
143
                    $context
144
                ) === false) {
145
                    return false;
146
                }
147
148
                $old_node_data = $statements_analyzer->node_data;
149
150
                $statements_analyzer->node_data = clone $statements_analyzer->node_data;
151
152
                ArrayAssignmentAnalyzer::analyze(
153
                    $statements_analyzer,
154
                    new PhpParser\Node\Expr\ArrayDimFetch(
155
                        $args[0]->value,
156
                        null,
157
                        $args[$i]->value->getAttributes()
158
                    ),
159
                    $context,
160
                    $args[$i]->value,
161
                    $statements_analyzer->node_data->getType($args[$i]->value) ?: Type::getMixed()
162
                );
163
164
                $statements_analyzer->node_data = $old_node_data;
165
            }
166
167
            return;
168
        }
169
170
        $context->inside_call = true;
171
172
        if (ExpressionAnalyzer::analyze(
173
            $statements_analyzer,
174
            $array_arg,
175
            $context
176
        ) === false) {
177
            return false;
178
        }
179
180
        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...
181
            if (ExpressionAnalyzer::analyze(
182
                $statements_analyzer,
183
                $args[$i]->value,
184
                $context
185
            ) === false) {
186
                return false;
187
            }
188
        }
189
190
        if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
191
            && $array_arg_type->hasArray()
192
        ) {
193
            /**
194
             * @psalm-suppress PossiblyUndefinedStringArrayOffset
195
             * @var TArray|ObjectLike|TList
196
             */
197
            $array_type = $array_arg_type->getAtomicTypes()['array'];
198
199
            $objectlike_list = null;
200
201
            if ($array_type instanceof ObjectLike) {
202
                if ($array_type->is_list) {
203
                    $objectlike_list = clone $array_type;
204
                }
205
206
                $array_type = $array_type->getGenericArrayType();
207
            }
208
209
            $by_ref_type = new Type\Union([clone $array_type]);
210
211
            foreach ($args as $argument_offset => $arg) {
212
                if ($argument_offset === 0) {
213
                    continue;
214
                }
215
216
                if (ExpressionAnalyzer::analyze(
217
                    $statements_analyzer,
218
                    $arg->value,
219
                    $context
220
                ) === false) {
221
                    return false;
222
                }
223
224
                if (!($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
225
                    || $arg_value_type->hasMixed()
226
                ) {
227
                    $by_ref_type = Type::combineUnionTypes(
228
                        $by_ref_type,
229
                        new Type\Union([new TArray([Type::getInt(), Type::getMixed()])])
230
                    );
231
                } elseif ($arg->unpack) {
232
                    $by_ref_type = Type::combineUnionTypes(
233
                        $by_ref_type,
234
                        clone $arg_value_type
235
                    );
236
                } else {
237
                    if ($objectlike_list) {
238
                        if ($is_push) {
239
                            \array_push($objectlike_list->properties, $arg_value_type);
240
                        } else {
241
                            \array_unshift($objectlike_list->properties, $arg_value_type);
242
                        }
243
244
                        $by_ref_type = new Type\Union([$objectlike_list]);
245
                    } elseif ($array_type instanceof TList) {
246
                        $by_ref_type = Type::combineUnionTypes(
247
                            $by_ref_type,
248
                            new Type\Union(
249
                                [
250
                                    new TNonEmptyList(clone $arg_value_type),
251
                                ]
252
                            )
253
                        );
254
                    } else {
255
                        $by_ref_type = Type::combineUnionTypes(
256
                            $by_ref_type,
257
                            new Type\Union(
258
                                [
259
                                    new TNonEmptyArray(
260
                                        [
261
                                            Type::getInt(),
262
                                            clone $arg_value_type
263
                                        ]
264
                                    ),
265
                                ]
266
                            )
267
                        );
268
                    }
269
                }
270
            }
271
272
            AssignmentAnalyzer::assignByRefParam(
273
                $statements_analyzer,
274
                $array_arg,
275
                $by_ref_type,
276
                $by_ref_type,
277
                $context,
278
                false
279
            );
280
        }
281
282
        $context->inside_call = false;
283
284
        return;
285
    }
286
287
    /**
288
     * @param   StatementsAnalyzer                      $statements_analyzer
289
     * @param   array<int, PhpParser\Node\Arg>          $args
290
     * @param   Context                                 $context
291
     *
292
     * @return  false|null
293
     */
294
    public static function handleSplice(
295
        StatementsAnalyzer $statements_analyzer,
296
        array $args,
297
        Context $context
298
    ) {
299
        $context->inside_call = true;
300
        $array_arg = $args[0]->value;
301
302
        if (ExpressionAnalyzer::analyze(
303
            $statements_analyzer,
304
            $array_arg,
305
            $context
306
        ) === false) {
307
            return false;
308
        }
309
310
        $offset_arg = $args[1]->value;
311
312
        if (ExpressionAnalyzer::analyze(
313
            $statements_analyzer,
314
            $offset_arg,
315
            $context
316
        ) === false) {
317
            return false;
318
        }
319
320
        if (!isset($args[2])) {
321
            return;
322
        }
323
324
        $length_arg = $args[2]->value;
325
326
        if (ExpressionAnalyzer::analyze(
327
            $statements_analyzer,
328
            $length_arg,
329
            $context
330
        ) === false) {
331
            return false;
332
        }
333
334
        if (!isset($args[3])) {
335
            return;
336
        }
337
338
        $replacement_arg = $args[3]->value;
339
340
        if (ExpressionAnalyzer::analyze(
341
            $statements_analyzer,
342
            $replacement_arg,
343
            $context
344
        ) === false) {
345
            return false;
346
        }
347
348
        $context->inside_call = false;
349
350
        $replacement_arg_type = $statements_analyzer->node_data->getType($replacement_arg);
351
352
        if ($replacement_arg_type
353
            && !$replacement_arg_type->hasArray()
354
            && $replacement_arg_type->hasString()
355
            && $replacement_arg_type->isSingle()
356
        ) {
357
            $replacement_arg_type = new Type\Union([
358
                new Type\Atomic\TArray([Type::getInt(), $replacement_arg_type])
359
            ]);
360
361
            $statements_analyzer->node_data->setType($replacement_arg, $replacement_arg_type);
362
        }
363
364
        if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
365
            && $array_arg_type->hasArray()
366
            && $replacement_arg_type
367
            && $replacement_arg_type->hasArray()
368
        ) {
369
            /**
370
             * @psalm-suppress PossiblyUndefinedStringArrayOffset
371
             * @var TArray|ObjectLike|TList
372
             */
373
            $array_type = $array_arg_type->getAtomicTypes()['array'];
374
375
            if ($array_type instanceof ObjectLike) {
376
                if ($array_type->is_list) {
377
                    $array_type = new TNonEmptyList($array_type->getGenericValueType());
378
                } else {
379
                    $array_type = $array_type->getGenericArrayType();
380
                }
381
            }
382
383
            if ($array_type instanceof TArray
384
                && $array_type->type_params[0]->hasInt()
385
                && !$array_type->type_params[0]->hasString()
386
            ) {
387
                if ($array_type instanceof TNonEmptyArray) {
388
                    $array_type = new TNonEmptyList($array_type->type_params[1]);
389
                } else {
390
                    $array_type = new TList($array_type->type_params[1]);
391
                }
392
            }
393
394
            /**
395
             * @psalm-suppress PossiblyUndefinedStringArrayOffset
396
             * @var TArray|ObjectLike|TList
397
             */
398
            $replacement_array_type = $replacement_arg_type->getAtomicTypes()['array'];
399
400
            $by_ref_type = TypeCombination::combineTypes([$array_type, $replacement_array_type]);
401
402
            AssignmentAnalyzer::assignByRefParam(
403
                $statements_analyzer,
404
                $array_arg,
405
                $by_ref_type,
406
                $by_ref_type,
407
                $context,
408
                false
409
            );
410
411
            return;
412
        }
413
414
        $array_type = Type::getArray();
415
416
        AssignmentAnalyzer::assignByRefParam(
417
            $statements_analyzer,
418
            $array_arg,
419
            $array_type,
420
            $array_type,
421
            $context,
422
            false
423
        );
424
    }
425
426
    /**
427
     * @return void
428
     */
429
    public static function handleByRefArrayAdjustment(
430
        StatementsAnalyzer $statements_analyzer,
431
        PhpParser\Node\Arg $arg,
432
        Context $context
433
    ) {
434
        $var_id = ExpressionIdentifier::getVarId(
435
            $arg->value,
436
            $statements_analyzer->getFQCLN(),
437
            $statements_analyzer
438
        );
439
440
        if ($var_id) {
441
            $context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
442
443
            if (isset($context->vars_in_scope[$var_id])) {
444
                $array_type = clone $context->vars_in_scope[$var_id];
445
446
                $array_atomic_types = $array_type->getAtomicTypes();
447
448
                foreach ($array_atomic_types as $array_atomic_type) {
449
                    if ($array_atomic_type instanceof ObjectLike) {
450
                        $array_atomic_type = $array_atomic_type->getGenericArrayType();
451
                    }
452
453
                    if ($array_atomic_type instanceof TNonEmptyArray) {
454 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...
455
                            if ($array_atomic_type->count === 0) {
456
                                $array_atomic_type = new TArray(
457
                                    [
458
                                        new Type\Union([new TEmpty]),
459
                                        new Type\Union([new TEmpty]),
460
                                    ]
461
                                );
462
                            } else {
463
                                $array_atomic_type->count--;
464
                            }
465
                        } else {
466
                            $array_atomic_type = new TArray($array_atomic_type->type_params);
467
                        }
468
469
                        $array_type->addType($array_atomic_type);
470
                        $context->removeDescendents($var_id, $array_type);
471
                    } elseif ($array_atomic_type instanceof TNonEmptyList) {
472 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...
473
                            if ($array_atomic_type->count === 0) {
474
                                $array_atomic_type = new TArray(
475
                                    [
476
                                        new Type\Union([new TEmpty]),
477
                                        new Type\Union([new TEmpty]),
478
                                    ]
479
                                );
480
                            } else {
481
                                $array_atomic_type->count--;
482
                            }
483
                        } else {
484
                            $array_atomic_type = new TList($array_atomic_type->type_param);
485
                        }
486
487
                        $array_type->addType($array_atomic_type);
488
                        $context->removeDescendents($var_id, $array_type);
489
                    }
490
                }
491
492
                $context->vars_in_scope[$var_id] = $array_type;
493
            }
494
        }
495
    }
496
497
    /**
498
     * @param  string   $method_id
499
     * @param  int      $min_closure_param_count
500
     * @param  int      $max_closure_param_count [description]
501
     * @param  (TArray|null)[] $array_arg_types
502
     *
503
     * @return void
504
     */
505
    private static function checkClosureType(
506
        StatementsAnalyzer $statements_analyzer,
507
        Context $context,
508
        $method_id,
509
        Type\Atomic $closure_type,
510
        PhpParser\Node\Arg $closure_arg,
511
        $min_closure_param_count,
512
        $max_closure_param_count,
513
        array $array_arg_types,
514
        bool $check_functions
515
    ) {
516
        $codebase = $statements_analyzer->getCodebase();
517
518
        if (!$closure_type instanceof Type\Atomic\TFn) {
519
            if ($method_id === 'array_map') {
520
                return;
521
            }
522
523
            if (!$closure_arg->value instanceof PhpParser\Node\Scalar\String_
524
                && !$closure_arg->value instanceof PhpParser\Node\Expr\Array_
525
                && !$closure_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat
526
            ) {
527
                return;
528
            }
529
530
            $function_ids = CallAnalyzer::getFunctionIdsFromCallableArg(
531
                $statements_analyzer,
532
                $closure_arg->value
533
            );
534
535
            $closure_types = [];
536
537
            foreach ($function_ids as $function_id) {
538
                $function_id = strtolower($function_id);
539
540
                if (strpos($function_id, '::') !== false) {
541
                    if ($function_id[0] === '$') {
542
                        $function_id = \substr($function_id, 1);
543
                    }
544
545
                    $function_id_parts = explode('&', $function_id);
546
547
                    foreach ($function_id_parts as $function_id_part) {
548
                        list($callable_fq_class_name, $method_name) = explode('::', $function_id_part);
549
550
                        switch ($callable_fq_class_name) {
551
                            case 'self':
552
                            case 'static':
553
                            case 'parent':
554
                                $container_class = $statements_analyzer->getFQCLN();
555
556
                                if ($callable_fq_class_name === 'parent') {
557
                                    $container_class = $statements_analyzer->getParentFQCLN();
558
                                }
559
560
                                if (!$container_class) {
561
                                    continue 2;
562
                                }
563
564
                                $callable_fq_class_name = $container_class;
565
                        }
566
567
                        if (!$codebase->classOrInterfaceExists($callable_fq_class_name)) {
568
                            return;
569
                        }
570
571
                        $function_id_part = new \Psalm\Internal\MethodIdentifier(
572
                            $callable_fq_class_name,
573
                            strtolower($method_name)
574
                        );
575
576
                        try {
577
                            $method_storage = $codebase->methods->getStorage($function_id_part);
578
                        } catch (\UnexpectedValueException $e) {
579
                            // the method may not exist, but we're suppressing that issue
580
                            continue;
581
                        }
582
583
                        $closure_types[] = new Type\Atomic\TFn(
584
                            'Closure',
585
                            $method_storage->params,
586
                            $method_storage->return_type ?: Type::getMixed()
587
                        );
588
                    }
589
                } else {
590
                    if (!$check_functions) {
591
                        continue;
592
                    }
593
594
                    if (!$codebase->functions->functionExists($statements_analyzer, $function_id)) {
595
                        continue;
596
                    }
597
598
                    $function_storage = $codebase->functions->getStorage(
599
                        $statements_analyzer,
600
                        $function_id
601
                    );
602
603
                    if (InternalCallMapHandler::inCallMap($function_id)) {
604
                        $callmap_callables = InternalCallMapHandler::getCallablesFromCallMap($function_id);
605
606
                        if ($callmap_callables === null) {
607
                            throw new \UnexpectedValueException('This should not happen');
608
                        }
609
610
                        $passing_callmap_callables = [];
611
612
                        foreach ($callmap_callables as $callmap_callable) {
613
                            $required_param_count = 0;
614
615
                            assert($callmap_callable->params !== null);
616
617 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...
618
                                if (!$param->is_optional && !$param->is_variadic) {
619
                                    $required_param_count = $i + 1;
620
                                }
621
                            }
622
623
                            if ($required_param_count <= $max_closure_param_count) {
624
                                $passing_callmap_callables[] = $callmap_callable;
625
                            }
626
                        }
627
628
                        if ($passing_callmap_callables) {
629
                            foreach ($passing_callmap_callables as $passing_callmap_callable) {
630
                                $closure_types[] = $passing_callmap_callable;
631
                            }
632
                        } else {
633
                            $closure_types[] = $callmap_callables[0];
634
                        }
635
                    } else {
636
                        $closure_types[] = new Type\Atomic\TFn(
637
                            'Closure',
638
                            $function_storage->params,
639
                            $function_storage->return_type ?: Type::getMixed()
640
                        );
641
                    }
642
                }
643
            }
644
        } else {
645
            $closure_types = [$closure_type];
646
        }
647
648
        foreach ($closure_types as $closure_type) {
649
            if ($closure_type->params === null) {
650
                continue;
651
            }
652
653
            self::checkClosureTypeArgs(
654
                $statements_analyzer,
655
                $context,
656
                $method_id,
657
                $closure_type,
658
                $closure_arg,
659
                $min_closure_param_count,
660
                $max_closure_param_count,
661
                $array_arg_types
662
            );
663
        }
664
    }
665
666
    /**
667
     * @param  Type\Atomic\TFn|Type\Atomic\TCallable $closure_type
668
     * @param  string   $method_id
669
     * @param  int      $min_closure_param_count
670
     * @param  int      $max_closure_param_count
671
     * @param  (TArray|null)[] $array_arg_types
672
     *
673
     * @return void
674
     */
675
    private static function checkClosureTypeArgs(
676
        StatementsAnalyzer $statements_analyzer,
677
        Context $context,
678
        $method_id,
679
        Type\Atomic $closure_type,
680
        PhpParser\Node\Arg $closure_arg,
681
        $min_closure_param_count,
682
        $max_closure_param_count,
683
        array $array_arg_types
684
    ) {
685
        $codebase = $statements_analyzer->getCodebase();
686
687
        $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...
688
689
        if ($closure_params === null) {
690
            throw new \UnexpectedValueException('Closure params should not be null here');
691
        }
692
693
        $required_param_count = 0;
694
695
        foreach ($closure_params as $i => $param) {
696
            if (!$param->is_optional && !$param->is_variadic) {
697
                $required_param_count = $i + 1;
698
            }
699
        }
700
701
        if (count($closure_params) < $min_closure_param_count) {
702
            $argument_text = $min_closure_param_count === 1 ? 'one argument' : $min_closure_param_count . ' arguments';
703
704
            if (IssueBuffer::accepts(
705
                new TooManyArguments(
706
                    'The callable passed to ' . $method_id . ' will be called with ' . $argument_text . ', expecting '
707
                        . $required_param_count,
708
                    new CodeLocation($statements_analyzer->getSource(), $closure_arg),
709
                    $method_id
710
                ),
711
                $statements_analyzer->getSuppressedIssues()
712
            )) {
713
                // fall through
714
            }
715
716
            return;
717
        } elseif ($required_param_count > $max_closure_param_count) {
718
            $argument_text = $max_closure_param_count === 1 ? 'one argument' : $max_closure_param_count . ' arguments';
719
720
            if (IssueBuffer::accepts(
721
                new TooFewArguments(
722
                    'The callable passed to ' . $method_id . ' will be called with ' . $argument_text . ', expecting '
723
                        . $required_param_count,
724
                    new CodeLocation($statements_analyzer->getSource(), $closure_arg),
725
                    $method_id
726
                ),
727
                $statements_analyzer->getSuppressedIssues()
728
            )) {
729
                // fall through
730
            }
731
732
            return;
733
        }
734
735
        // abandon attempt to validate closure params if we have an extra arg for ARRAY_FILTER
736
        if ($method_id === 'array_filter' && $max_closure_param_count > 1) {
737
            return;
738
        }
739
740
        foreach ($closure_params as $i => $closure_param) {
741
            if (!isset($array_arg_types[$i])) {
742
                continue;
743
            }
744
745
            $array_arg_type = $array_arg_types[$i];
746
747
            $input_type = $array_arg_type->type_params[1];
748
749
            if ($input_type->hasMixed()) {
750
                continue;
751
            }
752
753
            $closure_param_type = $closure_param->type;
754
755
            if (!$closure_param_type) {
756
                continue;
757
            }
758
759
            if ($method_id === 'array_map'
760
                && $i === 0
761
                && $closure_type->return_type
762
                && $closure_param_type->hasTemplate()
763
            ) {
764
                $closure_param_type = clone $closure_param_type;
765
                $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...
766
767
                $template_result = new \Psalm\Internal\Type\TemplateResult(
768
                    [],
769
                    []
770
                );
771
772
                foreach ($closure_param_type->getTemplateTypes() as $template_type) {
773
                    $template_result->template_types[$template_type->param_name] = [
774
                        ($template_type->defining_class) => [$template_type->as]
775
                    ];
776
                }
777
778
                $closure_param_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
779
                    $closure_param_type,
780
                    $template_result,
781
                    $codebase,
782
                    $statements_analyzer,
783
                    $input_type,
784
                    $i,
785
                    $context->self,
786
                    $context->calling_method_id ?: $context->calling_function_id
787
                );
788
789
                $closure_type->return_type->replaceTemplateTypesWithArgTypes(
790
                    $template_result,
791
                    $codebase
792
                );
793
            }
794
795
            $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
796
797
            $type_match_found = TypeAnalyzer::isContainedBy(
798
                $codebase,
799
                $input_type,
800
                $closure_param_type,
801
                $input_type->ignore_nullable_issues,
802
                $input_type->ignore_falsable_issues,
803
                $union_comparison_results
804
            );
805
806
            if ($union_comparison_results->type_coerced) {
807
                if ($union_comparison_results->type_coerced_from_mixed) {
808
                    if (IssueBuffer::accepts(
809
                        new MixedArgumentTypeCoercion(
810
                            'First parameter of closure passed to function ' . $method_id . ' expects ' .
811
                                $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided',
812
                            new CodeLocation($statements_analyzer->getSource(), $closure_arg),
813
                            $method_id
814
                        ),
815
                        $statements_analyzer->getSuppressedIssues()
816
                    )) {
817
                        // keep soldiering on
818
                    }
819
                } else {
820
                    if (IssueBuffer::accepts(
821
                        new ArgumentTypeCoercion(
822
                            'First parameter of closure passed to function ' . $method_id . ' expects ' .
823
                                $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided',
824
                            new CodeLocation($statements_analyzer->getSource(), $closure_arg),
825
                            $method_id
826
                        ),
827
                        $statements_analyzer->getSuppressedIssues()
828
                    )) {
829
                        // keep soldiering on
830
                    }
831
                }
832
            }
833
834
            if (!$union_comparison_results->type_coerced && !$type_match_found) {
835
                $types_can_be_identical = TypeAnalyzer::canExpressionTypesBeIdentical(
836
                    $codebase,
837
                    $input_type,
838
                    $closure_param_type
839
                );
840
841
                if ($union_comparison_results->scalar_type_match_found) {
842
                    if (IssueBuffer::accepts(
843
                        new InvalidScalarArgument(
844
                            'First parameter of closure passed to function ' . $method_id . ' expects ' .
845
                                $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided',
846
                            new CodeLocation($statements_analyzer->getSource(), $closure_arg),
847
                            $method_id
848
                        ),
849
                        $statements_analyzer->getSuppressedIssues()
850
                    )) {
851
                        // fall through
852
                    }
853
                } elseif ($types_can_be_identical) {
854
                    if (IssueBuffer::accepts(
855
                        new PossiblyInvalidArgument(
856
                            'First parameter of closure passed to function ' . $method_id . ' expects '
857
                                . $closure_param_type->getId() . ', possibly different type '
858
                                . $input_type->getId() . ' provided',
859
                            new CodeLocation($statements_analyzer->getSource(), $closure_arg),
860
                            $method_id
861
                        ),
862
                        $statements_analyzer->getSuppressedIssues()
863
                    )) {
864
                        // fall through
865
                    }
866
                } elseif (IssueBuffer::accepts(
867
                    new InvalidArgument(
868
                        'First parameter of closure passed to function ' . $method_id . ' expects ' .
869
                            $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided',
870
                        new CodeLocation($statements_analyzer->getSource(), $closure_arg),
871
                        $method_id
872
                    ),
873
                    $statements_analyzer->getSuppressedIssues()
874
                )) {
875
                    // fall through
876
                }
877
            }
878
        }
879
    }
880
}
881