ArgumentsAnalyzer   F
last analyzed

Complexity

Total Complexity 271

Size/Duplication

Total Lines 1054
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 38

Importance

Changes 0
Metric Value
dl 0
loc 1054
rs 1.3839
c 0
b 0
f 0
wmc 271
lcom 2
cbo 38

5 Methods

Rating   Name   Duplication   Size   Complexity  
F analyze() 0 307 87
F checkArgumentsMatch() 0 337 93
F handlePossiblyMatchingByRefParam() 0 140 34
F evaluateAribitraryParam() 0 107 36
F handleByRefFunctionArg() 0 120 21

How to fix   Complexity   

Complex Class

Complex classes like ArgumentsAnalyzer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArgumentsAnalyzer, and based on these observations, apply Extract Interface, too.

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