ArgumentsAnalyzer::checkArgumentsMatch()   F
last analyzed

Complexity

Conditions 93
Paths > 20000

Size

Total Lines 337

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 93
nc 371891520
nop 9
dl 0
loc 337
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
3
4
use PhpParser;
5
use Psalm\Codebase;
6
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
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\Statements\Expression\Fetch\ArrayFetchAnalyzer;
11
use Psalm\Internal\Analyzer\StatementsAnalyzer;
12
use Psalm\Internal\Analyzer\TypeAnalyzer;
13
use Psalm\Internal\Codebase\InternalCallMapHandler;
14
use Psalm\Internal\MethodIdentifier;
15
use Psalm\Internal\Type\TemplateResult;
16
use Psalm\Internal\Type\UnionTemplateHandler;
17
use Psalm\CodeLocation;
18
use Psalm\Context;
19
use Psalm\Issue\InvalidPassByReference;
20
use Psalm\Issue\PossiblyUndefinedVariable;
21
use Psalm\Issue\TooFewArguments;
22
use Psalm\Issue\TooManyArguments;
23
use Psalm\Issue\UndefinedVariable;
24
use Psalm\IssueBuffer;
25
use Psalm\Storage\ClassLikeStorage;
26
use Psalm\Storage\FunctionLikeParameter;
27
use Psalm\Storage\FunctionLikeStorage;
28
use Psalm\Type;
29
use Psalm\Type\Atomic\ObjectLike;
30
use Psalm\Type\Atomic\TArray;
31
use Psalm\Type\Atomic\TList;
32
use function strtolower;
33
use function strpos;
34
use function count;
35
use function in_array;
36
use function array_reverse;
37
use function is_string;
38
39
/**
40
 * @internal
41
 */
42
class ArgumentsAnalyzer
43
{
44
    /**
45
     * @param   StatementsAnalyzer                       $statements_analyzer
46
     * @param   array<int, PhpParser\Node\Arg>          $args
47
     * @param   array<int, FunctionLikeParameter>|null  $function_params
48
     * @param   array<string, array<string, array{Type\Union, 1?:int}>>|null   $generic_params
49
     * @param   string|null                             $method_id
50
     * @param   Context                                 $context
51
     *
52
     * @return  false|null
53
     */
54
    public static function analyze(
55
        StatementsAnalyzer $statements_analyzer,
56
        array $args,
57
        ?array $function_params,
58
        ?string $method_id,
59
        Context $context,
60
        ?TemplateResult $template_result = null
61
    ) {
62
        $last_param = $function_params
63
            ? $function_params[count($function_params) - 1]
64
            : null;
65
66
        // if this modifies the array type based on further args
67
        if ($method_id
68
            && in_array($method_id, ['array_push', 'array_unshift'], true)
69
            && $function_params
70
            && isset($args[0])
71
            && isset($args[1])
72
        ) {
73
            if (ArrayFunctionArgumentsAnalyzer::handleAddition(
74
                $statements_analyzer,
75
                $args,
76
                $context,
77
                $method_id === 'array_push'
78
            ) === false
79
            ) {
80
                return false;
81
            }
82
83
            return;
84
        }
85
86
        if ($method_id && $method_id === 'array_splice' && $function_params && count($args) > 1) {
87
            if (ArrayFunctionArgumentsAnalyzer::handleSplice($statements_analyzer, $args, $context) === false) {
88
                return false;
89
            }
90
91
            return;
92
        }
93
94
        if ($method_id === 'array_map') {
95
            $args = array_reverse($args, true);
96
        }
97
98
        foreach ($args as $argument_offset => $arg) {
99
            if ($function_params === null) {
100
                if (self::evaluateAribitraryParam(
101
                    $statements_analyzer,
102
                    $arg,
103
                    $context
104
                ) === false) {
105
                    return false;
106
                }
107
108
                continue;
109
            }
110
111
            $param = $argument_offset < count($function_params)
112
                ? $function_params[$argument_offset]
113
                : ($last_param && $last_param->is_variadic ? $last_param : null);
114
115
            $by_ref = $param && $param->by_ref;
116
117
            $by_ref_type = null;
118
119
            if ($by_ref && $param) {
120
                $by_ref_type = $param->type ? clone $param->type : Type::getMixed();
121
            }
122
123
            if ($by_ref
124
                && $by_ref_type
125
                && !($arg->value instanceof PhpParser\Node\Expr\Closure
126
                    || $arg->value instanceof PhpParser\Node\Expr\ConstFetch
127
                    || $arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
128
                    || $arg->value instanceof PhpParser\Node\Expr\FuncCall
129
                    || $arg->value instanceof PhpParser\Node\Expr\MethodCall
130
                    || $arg->value instanceof PhpParser\Node\Expr\StaticCall
131
                    || $arg->value instanceof PhpParser\Node\Expr\New_
132
                    || $arg->value instanceof PhpParser\Node\Expr\Assign
133
                    || $arg->value instanceof PhpParser\Node\Expr\Array_
134
                    || $arg->value instanceof PhpParser\Node\Expr\Ternary
135
                    || $arg->value instanceof PhpParser\Node\Expr\BinaryOp
136
                )
137
            ) {
138
                if (self::handleByRefFunctionArg(
139
                    $statements_analyzer,
140
                    $method_id,
141
                    $argument_offset,
142
                    $arg,
143
                    $context
144
                ) === false) {
145
                    return false;
146
                }
147
148
                continue;
149
            }
150
151
            $toggled_class_exists = false;
152
153
            if ($method_id === 'class_exists'
154
                && $argument_offset === 0
155
                && !$context->inside_class_exists
156
            ) {
157
                $context->inside_class_exists = true;
158
                $toggled_class_exists = true;
159
            }
160
161
            $codebase = $statements_analyzer->getCodebase();
162
163
            if (($arg->value instanceof PhpParser\Node\Expr\Closure
164
                    || $arg->value instanceof PhpParser\Node\Expr\ArrowFunction)
165
                && $template_result
166
                && $template_result->upper_bounds
167
                && $param
168
                && $param->type
169
                && !$arg->value->getDocComment()
170
            ) {
171
                if (($argument_offset === 1 && $method_id === 'array_filter' && count($args) === 2)
172
                    || ($argument_offset === 0 && $method_id === 'array_map' && count($args) >= 2)
173
                ) {
174
                    $function_like_params = [];
175
176
                    foreach ($template_result->upper_bounds as $template_name => $_) {
177
                        $function_like_params[] = new \Psalm\Storage\FunctionLikeParameter(
178
                            'function',
179
                            false,
180
                            new Type\Union([
181
                                new Type\Atomic\TTemplateParam(
182
                                    $template_name,
183
                                    Type::getMixed(),
184
                                    $method_id
185
                                )
186
                            ])
187
                        );
188
                    }
189
190
                    $replaced_type = new Type\Union([
191
                        new Type\Atomic\TCallable(
192
                            'callable',
193
                            array_reverse($function_like_params)
194
                        )
195
                    ]);
196
                } else {
197
                    $replaced_type = clone $param->type;
198
                }
199
200
                $replace_template_result = new \Psalm\Internal\Type\TemplateResult(
201
                    $template_result->upper_bounds,
202
                    []
203
                );
204
205
                $replaced_type = \Psalm\Internal\Type\UnionTemplateHandler::replaceTemplateTypesWithStandins(
206
                    $replaced_type,
207
                    $replace_template_result,
208
                    $codebase,
209
                    $statements_analyzer,
210
                    null,
211
                    null,
212
                    null,
213
                    'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
214
                );
215
216
                $replaced_type->replaceTemplateTypesWithArgTypes(
217
                    $replace_template_result,
218
                    $codebase
219
                );
220
221
                $closure_id = strtolower($statements_analyzer->getFilePath())
222
                    . ':' . $arg->value->getLine()
223
                    . ':' . (int)$arg->value->getAttribute('startFilePos')
224
                    . ':-:closure';
225
226
                try {
227
                    $closure_storage = $codebase->getClosureStorage(
228
                        $statements_analyzer->getFilePath(),
229
                        $closure_id
230
                    );
231
                } catch (\UnexpectedValueException $e) {
232
                    continue;
233
                }
234
235
                foreach ($replaced_type->getAtomicTypes() as $replaced_type_part) {
236
                    if ($replaced_type_part instanceof Type\Atomic\TCallable
237
                        || $replaced_type_part instanceof Type\Atomic\TFn
238
                    ) {
239
                        foreach ($closure_storage->params as $closure_param_offset => $param_storage) {
240
                            if (isset($replaced_type_part->params[$closure_param_offset]->type)
241
                                && !$replaced_type_part->params[$closure_param_offset]->type->hasTemplate()
242
                            ) {
243
                                if ($param_storage->type) {
244
                                    if ($method_id === 'array_map' || $method_id === 'array_filter') {
245
                                        ArrayFetchAnalyzer::taintArrayFetch(
246
                                            $statements_analyzer,
247
                                            $args[1 - $argument_offset]->value,
248
                                            null,
249
                                            $param_storage->type,
250
                                            Type::getMixed()
251
                                        );
252
                                    }
253
254
                                    if ($param_storage->type !== $param_storage->signature_type) {
255
                                        continue;
256
                                    }
257
258
                                    $type_match_found = TypeAnalyzer::isContainedBy(
259
                                        $codebase,
260
                                        $replaced_type_part->params[$closure_param_offset]->type,
261
                                        $param_storage->type
262
                                    );
263
264
                                    if (!$type_match_found) {
265
                                        continue;
266
                                    }
267
                                }
268
269
                                $param_storage->type = $replaced_type_part->params[$closure_param_offset]->type;
270
271
                                if ($method_id === 'array_map' || $method_id === 'array_filter') {
272
                                    ArrayFetchAnalyzer::taintArrayFetch(
273
                                        $statements_analyzer,
274
                                        $args[1 - $argument_offset]->value,
275
                                        null,
276
                                        $param_storage->type,
277
                                        Type::getMixed()
278
                                    );
279
                                }
280
                            }
281
                        }
282
                    }
283
                }
284
            }
285
286
            $was_inside_call = $context->inside_call;
287
288
            $context->inside_call = true;
289
290
            if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) {
291
                return false;
292
            }
293
294
            if (!$was_inside_call) {
295
                $context->inside_call = false;
296
            }
297
298
            if (($argument_offset === 0 && $method_id === 'array_filter' && count($args) === 2)
299
                || ($argument_offset > 0 && $method_id === 'array_map' && count($args) >= 2)
300
            ) {
301
                $generic_param_type = new Type\Union([
302
                    new Type\Atomic\TArray([
303
                        Type::getArrayKey(),
304
                        new Type\Union([
305
                            new Type\Atomic\TTemplateParam(
306
                                'ArrayValue' . $argument_offset,
307
                                Type::getMixed(),
308
                                $method_id
309
                            )
310
                        ])
311
                    ])
312
                ]);
313
314
                $template_types = ['ArrayValue' . $argument_offset => [$method_id => [Type::getMixed()]]];
315
316
                $replace_template_result = new \Psalm\Internal\Type\TemplateResult(
317
                    $template_types,
318
                    []
319
                );
320
321
                \Psalm\Internal\Type\UnionTemplateHandler::replaceTemplateTypesWithStandins(
322
                    $generic_param_type,
323
                    $replace_template_result,
324
                    $codebase,
325
                    $statements_analyzer,
326
                    $statements_analyzer->node_data->getType($arg->value),
327
                    $argument_offset,
328
                    'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
329
                );
330
331
                if ($replace_template_result->upper_bounds) {
332
                    if (!$template_result) {
333
                        $template_result = new TemplateResult([], []);
334
                    }
335
336
                    $template_result->upper_bounds += $replace_template_result->upper_bounds;
337
                }
338
            }
339
340
            if ($codebase->find_unused_variables
341
                && ($arg->value instanceof PhpParser\Node\Expr\AssignOp
342
                    || $arg->value instanceof PhpParser\Node\Expr\PreInc
343
                    || $arg->value instanceof PhpParser\Node\Expr\PreDec)
344
            ) {
345
                $var_id = ExpressionIdentifier::getVarId(
346
                    $arg->value->var,
347
                    $statements_analyzer->getFQCLN(),
348
                    $statements_analyzer
349
                );
350
351
                if ($var_id) {
352
                    $context->hasVariable($var_id, $statements_analyzer);
353
                }
354
            }
355
356
            if ($toggled_class_exists) {
357
                $context->inside_class_exists = false;
358
            }
359
        }
360
    }
361
362
    /**
363
     * @param   StatementsAnalyzer                       $statements_analyzer
364
     * @param   array<int, PhpParser\Node\Arg>          $args
365
     * @param   string|MethodIdentifier|null  $method_id
366
     * @param   array<int,FunctionLikeParameter>        $function_params
367
     * @param   FunctionLikeStorage|null                $function_storage
368
     * @param   ClassLikeStorage|null                   $class_storage
369
     * @param   CodeLocation                            $code_location
370
     * @param   Context                                 $context
371
     *
372
     * @return  false|null
373
     */
374
    public static function checkArgumentsMatch(
375
        StatementsAnalyzer $statements_analyzer,
376
        array $args,
377
        $method_id,
378
        array $function_params,
379
        $function_storage,
380
        $class_storage,
381
        ?TemplateResult $class_template_result,
382
        CodeLocation $code_location,
383
        Context $context
384
    ) {
385
        $in_call_map = $method_id ? InternalCallMapHandler::inCallMap((string) $method_id) : false;
386
387
        $cased_method_id = (string) $method_id;
388
389
        $is_variadic = false;
390
391
        $fq_class_name = null;
392
393
        $codebase = $statements_analyzer->getCodebase();
394
395
        if ($method_id) {
396
            if (!$in_call_map && $method_id instanceof \Psalm\Internal\MethodIdentifier) {
397
                $fq_class_name = $method_id->fq_class_name;
398
            }
399
400
            if ($function_storage) {
401
                $is_variadic = $function_storage->variadic;
402
            } elseif (is_string($method_id)) {
403
                $is_variadic = $codebase->functions->isVariadic(
404
                    $codebase,
405
                    strtolower($method_id),
406
                    $statements_analyzer->getRootFilePath()
407
                );
408
            } else {
409
                $is_variadic = $codebase->methods->isVariadic($method_id);
410
            }
411
        }
412
413
        if ($method_id instanceof \Psalm\Internal\MethodIdentifier) {
414
            $cased_method_id = $codebase->methods->getCasedMethodId($method_id);
415
        } elseif ($function_storage) {
416
            $cased_method_id = $function_storage->cased_name;
417
        }
418
419
        $calling_class_storage = $class_storage;
420
421
        $static_fq_class_name = $fq_class_name;
422
        $self_fq_class_name = $fq_class_name;
423
424
        if ($method_id instanceof \Psalm\Internal\MethodIdentifier) {
425
            $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
426
427
            if ($declaring_method_id && (string)$declaring_method_id !== (string)$method_id) {
428
                $self_fq_class_name = $declaring_method_id->fq_class_name;
429
                $class_storage = $codebase->classlike_storage_provider->get($self_fq_class_name);
430
            }
431
432
            $appearing_method_id = $codebase->methods->getAppearingMethodId($method_id);
433
434
            if ($appearing_method_id && $declaring_method_id !== $appearing_method_id) {
435
                $self_fq_class_name = $appearing_method_id->fq_class_name;
436
            }
437
        }
438
439
        if ($function_params) {
440
            foreach ($function_params as $function_param) {
441
                $is_variadic = $is_variadic || $function_param->is_variadic;
442
            }
443
        }
444
445
        $has_packed_var = false;
446
447
        foreach ($args as $arg) {
448
            $has_packed_var = $has_packed_var || $arg->unpack;
449
        }
450
451
        $last_param = $function_params
452
            ? $function_params[count($function_params) - 1]
453
            : null;
454
455
        $template_result = null;
456
457
        $class_generic_params = $class_template_result
458
            ? $class_template_result->upper_bounds
459
            : [];
460
461
        if ($function_storage) {
462
            $template_types = CallAnalyzer::getTemplateTypesForCall(
463
                $codebase,
464
                $class_storage,
465
                $self_fq_class_name,
466
                $calling_class_storage,
467
                $function_storage->template_types ?: []
468
            );
469
470
            if ($template_types) {
471
                $template_result = $class_template_result;
472
473
                if (!$template_result) {
474
                    $template_result = new TemplateResult($template_types, []);
475
                } elseif (!$template_result->template_types) {
476
                    $template_result->template_types = $template_types;
477
                }
478
479
                foreach ($args as $argument_offset => $arg) {
480
                    $function_param = count($function_params) > $argument_offset
481
                        ? $function_params[$argument_offset]
482
                        : ($last_param && $last_param->is_variadic ? $last_param : null);
483
484
                    if (!$function_param
485
                        || !$function_param->type
486
                    ) {
487
                        continue;
488
                    }
489
490
                    $arg_value_type = $statements_analyzer->node_data->getType($arg->value);
491
492
                    if (!$arg_value_type) {
493
                        continue;
494
                    }
495
496
                    UnionTemplateHandler::replaceTemplateTypesWithStandins(
497
                        $function_param->type,
498
                        $template_result,
499
                        $codebase,
500
                        $statements_analyzer,
501
                        $arg_value_type,
502
                        $argument_offset,
503
                        $context->self,
504
                        $context->calling_method_id ?: $context->calling_function_id,
505
                        false
506
                    );
507
508
                    if (!$class_template_result) {
509
                        $template_result->upper_bounds = [];
510
                    }
511
                }
512
            }
513
        }
514
515
        foreach ($class_generic_params as $template_name => $type_map) {
516
            foreach ($type_map as $class => $type) {
517
                $class_generic_params[$template_name][$class][0] = clone $type[0];
518
            }
519
        }
520
521
        $function_param_count = count($function_params);
522
523
        foreach ($args as $argument_offset => $arg) {
524
            $function_param = $function_param_count > $argument_offset
525
                ? $function_params[$argument_offset]
526
                : ($last_param && $last_param->is_variadic ? $last_param : null);
527
528
            if ($function_param
529
                && $function_param->by_ref
530
                && $method_id !== 'extract'
531
            ) {
532
                if (self::handlePossiblyMatchingByRefParam(
533
                    $statements_analyzer,
534
                    $codebase,
535
                    (string) $method_id,
536
                    $cased_method_id,
0 ignored issues
show
Bug introduced by
It seems like $cased_method_id defined by $codebase->methods->getCasedMethodId($method_id) on line 414 can also be of type object<Psalm\Internal\MethodIdentifier>; however, Psalm\Internal\Analyzer\...blyMatchingByRefParam() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
537
                    $last_param,
538
                    $function_params,
539
                    $argument_offset,
540
                    $arg,
541
                    $context,
542
                    $template_result
543
                ) === false) {
544
                    return;
545
                }
546
            }
547
548
            if ($method_id === 'compact'
549
                && ($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
550
                && $arg_value_type->isSingleStringLiteral()
551
            ) {
552
                $literal = $arg_value_type->getSingleStringLiteral();
553
554
                if (!$context->hasVariable('$' . $literal->value, $statements_analyzer)) {
555
                    if (IssueBuffer::accepts(
556
                        new UndefinedVariable(
557
                            'Cannot find referenced variable $' . $literal->value,
558
                            new CodeLocation($statements_analyzer->getSource(), $arg->value)
559
                        ),
560
                        $statements_analyzer->getSuppressedIssues()
561
                    )) {
562
                        // fall through
563
                    }
564
                }
565
            }
566
567
            if (ArgumentAnalyzer::checkArgumentMatches(
568
                $statements_analyzer,
569
                $cased_method_id,
570
                $self_fq_class_name,
571
                $static_fq_class_name,
572
                $code_location,
573
                $function_param,
574
                $argument_offset,
575
                $arg,
576
                $context,
577
                $class_generic_params,
578
                $template_result,
579
                $function_storage ? $function_storage->specialize_call : true,
580
                $in_call_map
581
            ) === false) {
582
                return false;
583
            }
584
        }
585
586
        if ($method_id === 'array_map' || $method_id === 'array_filter') {
587
            if ($method_id === 'array_map' && count($args) < 2) {
588
                if (IssueBuffer::accepts(
589
                    new TooFewArguments(
590
                        'Too few arguments for ' . $method_id,
591
                        $code_location,
592
                        $method_id
593
                    ),
594
                    $statements_analyzer->getSuppressedIssues()
595
                )) {
596
                    // fall through
597
                }
598
            } elseif ($method_id === 'array_filter' && count($args) < 1) {
599
                if (IssueBuffer::accepts(
600
                    new TooFewArguments(
601
                        'Too few arguments for ' . $method_id,
602
                        $code_location,
603
                        $method_id
604
                    ),
605
                    $statements_analyzer->getSuppressedIssues()
606
                )) {
607
                    // fall through
608
                }
609
            }
610
611
            if (ArrayFunctionArgumentsAnalyzer::checkArgumentsMatch(
612
                $statements_analyzer,
613
                $context,
614
                $args,
615
                $method_id,
616
                $context->check_functions
617
            ) === false
618
            ) {
619
                return false;
620
            }
621
622
            return null;
623
        }
624
625
        if (!$is_variadic
626
            && count($args) > count($function_params)
627
            && (!count($function_params) || $function_params[count($function_params) - 1]->name !== '...=')
628
            && ($in_call_map
629
                || !$function_storage instanceof \Psalm\Storage\MethodStorage
630
                || $function_storage->is_static
631
                || ($method_id instanceof MethodIdentifier
632
                    && $method_id->method_name === '__construct'))
633
        ) {
634
            if (IssueBuffer::accepts(
635
                new TooManyArguments(
636
                    'Too many arguments for ' . ($cased_method_id ?: $method_id)
637
                        . ' - expecting ' . count($function_params) . ' but saw ' . count($args),
638
                    $code_location,
639
                    (string) $method_id
640
                ),
641
                $statements_analyzer->getSuppressedIssues()
642
            )) {
643
                // fall through
644
            }
645
646
            return null;
647
        }
648
649
        if (!$has_packed_var && count($args) < count($function_params)) {
650
            if ($function_storage) {
651
                $expected_param_count = $function_storage->required_param_count;
652
            } else {
653
                for ($i = 0, $j = count($function_params); $i < $j; ++$i) {
654
                    $param = $function_params[$i];
655
656
                    if ($param->is_optional || $param->is_variadic) {
657
                        break;
658
                    }
659
                }
660
661
                $expected_param_count = $i;
662
            }
663
664
            for ($i = count($args), $j = count($function_params); $i < $j; ++$i) {
665
                $param = $function_params[$i];
666
667
                if (!$param->is_optional
668
                    && !$param->is_variadic
669
                    && ($in_call_map
670
                        || !$function_storage instanceof \Psalm\Storage\MethodStorage
671
                        || $function_storage->is_static
672
                        || ($method_id instanceof MethodIdentifier
673
                            && $method_id->method_name === '__construct'))
674
                ) {
675
                    if (IssueBuffer::accepts(
676
                        new TooFewArguments(
677
                            'Too few arguments for ' . $cased_method_id
678
                                . ' - expecting ' . $expected_param_count . ' but saw ' . count($args),
679
                            $code_location,
680
                            (string) $method_id
681
                        ),
682
                        $statements_analyzer->getSuppressedIssues()
683
                    )) {
684
                        // fall through
685
                    }
686
687
                    break;
688
                }
689
690
                if ($param->is_optional
691
                    && $param->type
692
                    && $param->default_type
693
                    && !$param->is_variadic
694
                    && $template_result
695
                ) {
696
                    UnionTemplateHandler::replaceTemplateTypesWithStandins(
697
                        $param->type,
698
                        $template_result,
699
                        $codebase,
700
                        $statements_analyzer,
701
                        clone $param->default_type,
702
                        $i,
703
                        $context->self,
704
                        $context->calling_method_id ?: $context->calling_function_id,
705
                        true
706
                    );
707
                }
708
            }
709
        }
710
    }
711
712
    /**
713
     * @param  string|null $method_id
714
     * @param  string|null $cased_method_id
715
     * @param  FunctionLikeParameter|null $last_param
716
     * @param  array<int, FunctionLikeParameter> $function_params
717
     * @return false|null
718
     */
719
    private static function handlePossiblyMatchingByRefParam(
720
        StatementsAnalyzer $statements_analyzer,
721
        Codebase $codebase,
722
        $method_id,
723
        $cased_method_id,
724
        $last_param,
725
        $function_params,
726
        int $argument_offset,
727
        PhpParser\Node\Arg $arg,
728
        Context $context,
729
        ?TemplateResult $template_result
730
    ) {
731
        if ($arg->value instanceof PhpParser\Node\Scalar
732
            || $arg->value instanceof PhpParser\Node\Expr\Cast
733
            || $arg->value instanceof PhpParser\Node\Expr\Array_
734
            || $arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
735
            || $arg->value instanceof PhpParser\Node\Expr\BinaryOp
736
            || $arg->value instanceof PhpParser\Node\Expr\Ternary
737
            || (
738
                (
739
                $arg->value instanceof PhpParser\Node\Expr\ConstFetch
740
                    || $arg->value instanceof PhpParser\Node\Expr\FuncCall
741
                    || $arg->value instanceof PhpParser\Node\Expr\MethodCall
742
                    || $arg->value instanceof PhpParser\Node\Expr\StaticCall
743
                ) && (
744
                    !($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
745
                    || !$arg_value_type->by_ref
746
                )
747
            )
748
        ) {
749
            if (IssueBuffer::accepts(
750
                new InvalidPassByReference(
751
                    'Parameter ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' expects a variable',
752
                    new CodeLocation($statements_analyzer->getSource(), $arg->value)
753
                ),
754
                $statements_analyzer->getSuppressedIssues()
755
            )) {
756
                // fall through
757
            }
758
759
            return false;
760
        }
761
762
        if (!in_array(
763
            $method_id,
764
            [
765
                'ksort', 'asort', 'krsort', 'arsort', 'natcasesort', 'natsort',
766
                'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift',
767
                'array_push', 'array_unshift', 'socket_select', 'array_splice',
768
            ],
769
            true
770
        )) {
771
            $by_ref_type = null;
772
            $by_ref_out_type = null;
773
774
            $check_null_ref = true;
775
776
            if ($last_param) {
777
                if ($argument_offset < count($function_params)) {
778
                    $function_param = $function_params[$argument_offset];
779
                } else {
780
                    $function_param = $last_param;
781
                }
782
783
                $by_ref_type = $function_param->type;
784
                $by_ref_out_type = $function_param->out_type;
785
786
                if ($by_ref_type && $by_ref_type->isNullable()) {
787
                    $check_null_ref = false;
788
                }
789
790
                if ($template_result && $by_ref_type) {
791
                    $original_by_ref_type = clone $by_ref_type;
792
793
                    $by_ref_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
794
                        clone $by_ref_type,
795
                        $template_result,
796
                        $codebase,
797
                        $statements_analyzer,
798
                        $statements_analyzer->node_data->getType($arg->value),
799
                        $argument_offset,
800
                        'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
801
                    );
802
803
                    if ($template_result->upper_bounds) {
804
                        $original_by_ref_type->replaceTemplateTypesWithArgTypes(
805
                            $template_result,
806
                            $codebase
807
                        );
808
809
                        $by_ref_type = $original_by_ref_type;
810
                    }
811
                }
812
813
                if ($template_result && $by_ref_out_type) {
814
                    $original_by_ref_out_type = clone $by_ref_out_type;
815
816
                    $by_ref_out_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
817
                        clone $by_ref_out_type,
818
                        $template_result,
819
                        $codebase,
820
                        $statements_analyzer,
821
                        $statements_analyzer->node_data->getType($arg->value),
822
                        $argument_offset,
823
                        'fn-' . ($context->calling_method_id ?: $context->calling_function_id)
824
                    );
825
826
                    if ($template_result->upper_bounds) {
827
                        $original_by_ref_out_type->replaceTemplateTypesWithArgTypes(
828
                            $template_result,
829
                            $codebase
830
                        );
831
832
                        $by_ref_out_type = $original_by_ref_out_type;
833
                    }
834
                }
835
836
                if ($by_ref_type && $function_param->is_variadic && $arg->unpack) {
837
                    $by_ref_type = new Type\Union([
838
                        new Type\Atomic\TArray([
839
                            Type::getInt(),
840
                            $by_ref_type,
841
                        ]),
842
                    ]);
843
                }
844
            }
845
846
            $by_ref_type = $by_ref_type ?: Type::getMixed();
847
848
            AssignmentAnalyzer::assignByRefParam(
849
                $statements_analyzer,
850
                $arg->value,
851
                $by_ref_type,
852
                $by_ref_out_type ?: $by_ref_type,
853
                $context,
854
                $method_id && (strpos($method_id, '::') !== false || !InternalCallMapHandler::inCallMap($method_id)),
855
                $check_null_ref
856
            );
857
        }
858
    }
859
860
    /**
861
     * @return false|null
862
     */
863
    private static function evaluateAribitraryParam(
864
        StatementsAnalyzer $statements_analyzer,
865
        PhpParser\Node\Arg $arg,
866
        Context $context
867
    ) {
868
        // there are a bunch of things we want to evaluate even when we don't
869
        // know what function/method is being called
870
        if ($arg->value instanceof PhpParser\Node\Expr\Closure
871
            || $arg->value instanceof PhpParser\Node\Expr\ConstFetch
872
            || $arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
873
            || $arg->value instanceof PhpParser\Node\Expr\FuncCall
874
            || $arg->value instanceof PhpParser\Node\Expr\MethodCall
875
            || $arg->value instanceof PhpParser\Node\Expr\StaticCall
876
            || $arg->value instanceof PhpParser\Node\Expr\New_
877
            || $arg->value instanceof PhpParser\Node\Expr\Cast
878
            || $arg->value instanceof PhpParser\Node\Expr\Assign
879
            || $arg->value instanceof PhpParser\Node\Expr\ArrayDimFetch
880
            || $arg->value instanceof PhpParser\Node\Expr\PropertyFetch
881
            || $arg->value instanceof PhpParser\Node\Expr\Array_
882
            || $arg->value instanceof PhpParser\Node\Expr\BinaryOp
883
            || $arg->value instanceof PhpParser\Node\Expr\Ternary
884
            || $arg->value instanceof PhpParser\Node\Scalar\Encapsed
885
            || $arg->value instanceof PhpParser\Node\Expr\PostInc
886
            || $arg->value instanceof PhpParser\Node\Expr\PostDec
887
            || $arg->value instanceof PhpParser\Node\Expr\PreInc
888
            || $arg->value instanceof PhpParser\Node\Expr\PreDec
889
        ) {
890
            $was_inside_call = $context->inside_call;
891
            $context->inside_call = true;
892
893
            if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) {
894
                return false;
895
            }
896
897
            if (!$was_inside_call) {
898
                $context->inside_call = false;
899
            }
900
        }
901
902
        if ($arg->value instanceof PhpParser\Node\Expr\PropertyFetch
903
            && $arg->value->name instanceof PhpParser\Node\Identifier
904
        ) {
905
            $var_id = '$' . $arg->value->name->name;
906
        } else {
907
            $var_id = ExpressionIdentifier::getVarId(
908
                $arg->value,
909
                $statements_analyzer->getFQCLN(),
910
                $statements_analyzer
911
            );
912
        }
913
914
        if ($var_id) {
915
            if (!$context->hasVariable($var_id, $statements_analyzer)
916
                || $context->vars_in_scope[$var_id]->isNull()
917
            ) {
918
                if (!isset($context->vars_in_scope[$var_id])
919
                    && $arg->value instanceof PhpParser\Node\Expr\Variable
920
                ) {
921
                    if (IssueBuffer::accepts(
922
                        new PossiblyUndefinedVariable(
923
                            'Variable ' . $var_id
924
                                . ' must be defined prior to use within an unknown function or method',
925
                            new CodeLocation($statements_analyzer->getSource(), $arg->value)
926
                        ),
927
                        $statements_analyzer->getSuppressedIssues()
928
                    )) {
929
                        // fall through
930
                    }
931
                }
932
933
                // we don't know if it exists, assume it's passed by reference
934
                $context->vars_in_scope[$var_id] = Type::getMixed();
935
                $context->vars_possibly_in_scope[$var_id] = true;
936
937
                if (strpos($var_id, '-') === false
938
                    && strpos($var_id, '[') === false
939
                    && !$statements_analyzer->hasVariable($var_id)
940
                ) {
941
                    $location = new CodeLocation($statements_analyzer, $arg->value);
942
                    $statements_analyzer->registerVariable(
943
                        $var_id,
944
                        $location,
945
                        null
946
                    );
947
948
                    $statements_analyzer->registerVariableUses([$location->getHash() => $location]);
949
                }
950
            } else {
951
                $context->removeVarFromConflictingClauses(
952
                    $var_id,
953
                    $context->vars_in_scope[$var_id],
954
                    $statements_analyzer
955
                );
956
957
                foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $type) {
958
                    if ($type instanceof TArray && $type->type_params[1]->isEmpty()) {
959
                        $context->vars_in_scope[$var_id]->removeType('array');
960
                        $context->vars_in_scope[$var_id]->addType(
961
                            new TArray(
962
                                [Type::getArrayKey(), Type::getMixed()]
963
                            )
964
                        );
965
                    }
966
                }
967
            }
968
        }
969
    }
970
971
    /**
972
     * @param string|null $method_id
973
     * @return false|null
974
     */
975
    private static function handleByRefFunctionArg(
976
        StatementsAnalyzer $statements_analyzer,
977
        $method_id,
978
        int $argument_offset,
979
        PhpParser\Node\Arg $arg,
980
        Context $context
981
    ) {
982
        $var_id = ExpressionIdentifier::getVarId(
983
            $arg->value,
984
            $statements_analyzer->getFQCLN(),
985
            $statements_analyzer
986
        );
987
988
        $builtin_array_functions = [
989
            'ksort', 'asort', 'krsort', 'arsort', 'natcasesort', 'natsort',
990
            'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift',
991
        ];
992
993
        if (($var_id && isset($context->vars_in_scope[$var_id]))
994
            || ($method_id
995
                && in_array(
996
                    $method_id,
997
                    $builtin_array_functions,
998
                    true
999
                ))
1000
        ) {
1001
            $was_inside_assignment = $context->inside_assignment;
1002
            $context->inside_assignment = true;
1003
1004
            // if the variable is in scope, get or we're in a special array function,
1005
            // figure out its type before proceeding
1006
            if (ExpressionAnalyzer::analyze(
1007
                $statements_analyzer,
1008
                $arg->value,
1009
                $context
1010
            ) === false) {
1011
                return false;
1012
            }
1013
1014
            $context->inside_assignment = $was_inside_assignment;
1015
        }
1016
1017
        // special handling for array sort
1018
        if ($argument_offset === 0
1019
            && $method_id
1020
            && in_array(
1021
                $method_id,
1022
                $builtin_array_functions,
1023
                true
1024
            )
1025
        ) {
1026
            if (in_array($method_id, ['array_pop', 'array_shift'], true)) {
1027
                ArrayFunctionArgumentsAnalyzer::handleByRefArrayAdjustment($statements_analyzer, $arg, $context);
1028
1029
                return;
1030
            }
1031
1032
            // noops
1033
            if (in_array($method_id, ['reset', 'end', 'next', 'prev', 'ksort'], true)) {
1034
                return;
1035
            }
1036
1037
            if (($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
1038
                && $arg_value_type->hasArray()
1039
            ) {
1040
                /**
1041
                 * @psalm-suppress PossiblyUndefinedStringArrayOffset
1042
                 * @var TArray|TList|ObjectLike
1043
                 */
1044
                $array_type = $arg_value_type->getAtomicTypes()['array'];
1045
1046
                if ($array_type instanceof ObjectLike) {
1047
                    $array_type = $array_type->getGenericArrayType();
1048
                }
1049
1050
                if ($array_type instanceof TList) {
1051
                    $array_type = new TArray([Type::getInt(), $array_type->type_param]);
1052
                }
1053
1054
                $by_ref_type = new Type\Union([clone $array_type]);
1055
1056
                AssignmentAnalyzer::assignByRefParam(
1057
                    $statements_analyzer,
1058
                    $arg->value,
1059
                    $by_ref_type,
1060
                    $by_ref_type,
1061
                    $context,
1062
                    false
1063
                );
1064
1065
                return;
1066
            }
1067
        }
1068
1069
        if ($method_id === 'socket_select') {
1070
            if (ExpressionAnalyzer::analyze(
1071
                $statements_analyzer,
1072
                $arg->value,
1073
                $context
1074
            ) === false) {
1075
                return false;
1076
            }
1077
        }
1078
1079
        if (!$arg->value instanceof PhpParser\Node\Expr\Variable) {
1080
            $suppressed_issues = $statements_analyzer->getSuppressedIssues();
1081
1082
            if (!in_array('EmptyArrayAccess', $suppressed_issues, true)) {
1083
                $statements_analyzer->addSuppressedIssues(['EmptyArrayAccess']);
1084
            }
1085
1086
            if (ExpressionAnalyzer::analyze($statements_analyzer, $arg->value, $context) === false) {
1087
                return false;
1088
            }
1089
1090
            if (!in_array('EmptyArrayAccess', $suppressed_issues, true)) {
1091
                $statements_analyzer->removeSuppressedIssues(['EmptyArrayAccess']);
1092
            }
1093
        }
1094
    }
1095
}
1096