FunctionCallAnalyzer::getAnalyzeNamedExpression()   F
last analyzed

Complexity

Conditions 44
Paths 31

Size

Total Lines 217

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 44
nc 31
nop 6
dl 0
loc 217
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\FunctionAnalyzer;
6
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
7
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
8
use Psalm\Internal\Analyzer\Statements\Expression\AssertionFinder;
9
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
10
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
11
use Psalm\Internal\Analyzer\StatementsAnalyzer;
12
use Psalm\Internal\Analyzer\TypeAnalyzer;
13
use Psalm\Internal\Codebase\InternalCallMapHandler;
14
use Psalm\CodeLocation;
15
use Psalm\Context;
16
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
17
use Psalm\Internal\Taint\Source;
18
use Psalm\Internal\Taint\TaintNode;
19
use Psalm\Issue\DeprecatedFunction;
20
use Psalm\Issue\ForbiddenCode;
21
use Psalm\Issue\MixedFunctionCall;
22
use Psalm\Issue\InvalidFunctionCall;
23
use Psalm\Issue\ImpureFunctionCall;
24
use Psalm\Issue\NullFunctionCall;
25
use Psalm\Issue\PossiblyInvalidFunctionCall;
26
use Psalm\Issue\PossiblyNullFunctionCall;
27
use Psalm\Issue\UnusedFunctionCall;
28
use Psalm\IssueBuffer;
29
use Psalm\Storage\Assertion;
30
use Psalm\Storage\FunctionLikeStorage;
31
use Psalm\Type;
32
use Psalm\Type\Atomic\TCallableObject;
33
use Psalm\Type\Atomic\TCallableString;
34
use Psalm\Type\Atomic\TTemplateParam;
35
use Psalm\Type\Atomic\TMixed;
36
use Psalm\Type\Atomic\TNamedObject;
37
use Psalm\Type\Atomic\TNull;
38
use Psalm\Type\Atomic\TString;
39
use Psalm\Type\Algebra;
40
use Psalm\Type\Reconciler;
41
use function count;
42
use function in_array;
43
use function reset;
44
use function implode;
45
use function strtolower;
46
use function array_merge;
47
use function is_string;
48
use function array_map;
49
use function extension_loaded;
50
use function strpos;
51
use Psalm\Internal\Type\TemplateResult;
52
use Psalm\Storage\FunctionLikeParameter;
53
use function explode;
54
55
/**
56
 * @internal
57
 */
58
class FunctionCallAnalyzer extends CallAnalyzer
59
{
60
    public static function analyze(
61
        StatementsAnalyzer $statements_analyzer,
62
        PhpParser\Node\Expr\FuncCall $stmt,
63
        Context $context
64
    ) : bool {
65
        $function_name = $stmt->name;
66
67
        $function_id = null;
68
        $function_params = null;
69
        $in_call_map = false;
70
71
        $is_stubbed = false;
72
73
        $function_storage = null;
74
75
        $codebase = $statements_analyzer->getCodebase();
76
77
        $code_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
78
        $codebase_functions = $codebase->functions;
79
        $config = $codebase->config;
80
        $defined_constants = [];
81
        $global_variables = [];
82
83
        $function_exists = false;
84
85
        $real_stmt = $stmt;
86
87
        if ($function_name instanceof PhpParser\Node\Name
88
            && isset($stmt->args[0])
89
            && !$stmt->args[0]->unpack
90
        ) {
91
            $original_function_id = implode('\\', $function_name->parts);
92
93
            if ($original_function_id === 'call_user_func') {
94
                $other_args = \array_slice($stmt->args, 1);
95
96
                $function_name = $stmt->args[0]->value;
97
98
                $stmt = new PhpParser\Node\Expr\FuncCall(
99
                    $function_name,
100
                    $other_args,
101
                    $stmt->getAttributes()
102
                );
103
            }
104
105
            if ($original_function_id === 'call_user_func_array' && isset($stmt->args[1])) {
106
                $function_name = $stmt->args[0]->value;
107
108
                $stmt = new PhpParser\Node\Expr\FuncCall(
109
                    $function_name,
110
                    [new PhpParser\Node\Arg($stmt->args[1]->value, false, true)],
111
                    $stmt->getAttributes()
112
                );
113
            }
114
        }
115
116
        $byref_uses = [];
117
118
        if ($function_name instanceof PhpParser\Node\Expr) {
119
            list($expr_function_exists, $expr_function_name, $expr_function_params, $byref_uses)
120
                = self::getAnalyzeNamedExpression(
121
                    $statements_analyzer,
122
                    $codebase,
123
                    $stmt,
124
                    $real_stmt,
125
                    $function_name,
126
                    $context
127
                );
128
129
            if ($expr_function_exists === false) {
130
                return true;
131
            }
132
133
            if ($expr_function_exists === true) {
134
                $function_exists = true;
135
            }
136
137
            if ($expr_function_name) {
138
                $function_name = $expr_function_name;
139
            }
140
141
            if ($expr_function_params) {
142
                $function_params = $expr_function_params;
143
            }
144
        } else {
145
            $original_function_id = implode('\\', $function_name->parts);
146
147
            if (!$function_name instanceof PhpParser\Node\Name\FullyQualified) {
148
                $function_id = $codebase_functions->getFullyQualifiedFunctionNameFromString(
149
                    $original_function_id,
150
                    $statements_analyzer
151
                );
152
            } else {
153
                $function_id = $original_function_id;
154
            }
155
156
            $namespaced_function_exists = $codebase_functions->functionExists(
157
                $statements_analyzer,
158
                strtolower($function_id)
159
            );
160
161
            if (!$namespaced_function_exists
162
                && !$function_name instanceof PhpParser\Node\Name\FullyQualified
163
            ) {
164
                $in_call_map = InternalCallMapHandler::inCallMap($original_function_id);
165
                $is_stubbed = $codebase_functions->hasStubbedFunction($original_function_id);
166
167
                if ($is_stubbed || $in_call_map) {
168
                    $function_id = $original_function_id;
169
                }
170
            } else {
171
                $in_call_map = InternalCallMapHandler::inCallMap($function_id);
172
                $is_stubbed = $codebase_functions->hasStubbedFunction($function_id);
173
            }
174
175
            if ($is_stubbed || $in_call_map || $namespaced_function_exists) {
176
                $function_exists = true;
177
            }
178
179
            if ($function_exists
180
                && $codebase->store_node_types
181
                && !$context->collect_initializations
182
                && !$context->collect_mutations
183
            ) {
184
                ArgumentMapPopulator::recordArgumentPositions(
185
                    $statements_analyzer,
186
                    $stmt,
187
                    $codebase,
188
                    $function_id
189
                );
190
            }
191
192
            $is_predefined = true;
193
194
            $is_maybe_root_function = !$function_name instanceof PhpParser\Node\Name\FullyQualified
195
                && count($function_name->parts) === 1;
196
197
            if (!$in_call_map) {
198
                $predefined_functions = $config->getPredefinedFunctions();
199
                $is_predefined = isset($predefined_functions[strtolower($original_function_id)])
200
                    || isset($predefined_functions[strtolower($function_id)]);
201
202
                if ($context->check_functions) {
203
                    if (self::checkFunctionExists(
204
                        $statements_analyzer,
205
                        $function_id,
206
                        $code_location,
207
                        $is_maybe_root_function
208
                    ) === false
209
                    ) {
210
                        if (ArgumentsAnalyzer::analyze(
211
                            $statements_analyzer,
212
                            $stmt->args,
213
                            null,
214
                            null,
215
                            $context
216
                        ) === false) {
217
                            // fall through
218
                        }
219
220
                        return true;
221
                    }
222
                }
223
            } else {
224
                $function_exists = true;
225
            }
226
227
            if ($function_exists) {
228
                $function_params = null;
229
230
                if ($codebase->functions->params_provider->has($function_id)) {
231
                    $function_params = $codebase->functions->params_provider->getFunctionParams(
232
                        $statements_analyzer,
233
                        $function_id,
234
                        $stmt->args
235
                    );
236
                }
237
238
                if ($function_params === null) {
239
                    if (!$in_call_map || $is_stubbed) {
240
                        try {
241
                            $function_storage = $codebase_functions->getStorage(
242
                                $statements_analyzer,
243
                                strtolower($function_id)
244
                            );
245
246
                            $function_params = $function_storage->params;
247
248
                            if (!$is_predefined) {
249
                                $defined_constants = $function_storage->defined_constants;
250
                                $global_variables = $function_storage->global_variables;
251
                            }
252
                        } catch (\UnexpectedValueException $e) {
253
                            $function_params = [
254
                                new FunctionLikeParameter('args', false, null, null, null, false, false, true)
255
                            ];
256
                        }
257
                    } else {
258
                        $function_callable = InternalCallMapHandler::getCallableFromCallMapById(
259
                            $codebase,
260
                            $function_id,
261
                            $stmt->args,
262
                            $statements_analyzer->node_data
263
                        );
264
265
                        $function_params = $function_callable->params;
266
                    }
267
                }
268
269
                if ($codebase->store_node_types
270
                    && !$context->collect_initializations
271
                    && !$context->collect_mutations
272
                ) {
273
                    $codebase->analyzer->addNodeReference(
274
                        $statements_analyzer->getFilePath(),
275
                        $function_name,
276
                        $function_id . '()'
277
                    );
278
                }
279
            }
280
        }
281
282
        $set_inside_conditional = false;
283
284
        if ($function_name instanceof PhpParser\Node\Name
285
            && $function_name->parts === ['assert']
286
            && !$context->inside_conditional
287
        ) {
288
            $context->inside_conditional = true;
289
            $set_inside_conditional = true;
290
        }
291
292
        if (ArgumentsAnalyzer::analyze(
293
            $statements_analyzer,
294
            $stmt->args,
295
            $function_params,
296
            $function_id,
297
            $context
298
        ) === false) {
299
            // fall through
300
        }
301
302
        if ($set_inside_conditional) {
303
            $context->inside_conditional = false;
304
        }
305
306
        $template_result = null;
307
308
        if ($function_exists) {
309
            if ($function_name instanceof PhpParser\Node\Name && $function_id) {
310
                if (!$is_stubbed && $in_call_map) {
311
                    $function_callable = \Psalm\Internal\Codebase\InternalCallMapHandler::getCallableFromCallMapById(
312
                        $codebase,
313
                        $function_id,
314
                        $stmt->args,
315
                        $statements_analyzer->node_data
316
                    );
317
318
                    $function_params = $function_callable->params;
319
                }
320
            }
321
322
            $template_result = new TemplateResult([], []);
323
324
            // do this here to allow closure param checks
325
            if ($function_params !== null
326
                && ArgumentsAnalyzer::checkArgumentsMatch(
327
                    $statements_analyzer,
328
                    $stmt->args,
329
                    $function_id,
330
                    $function_params,
331
                    $function_storage,
332
                    null,
333
                    $template_result,
334
                    $code_location,
335
                    $context
336
                ) === false
337
            ) {
338
                // fall through
339
            }
340
341
            CallAnalyzer::checkTemplateResult(
342
                $statements_analyzer,
343
                $template_result,
344
                $code_location,
345
                $function_id
346
            );
347
348
            if ($function_name instanceof PhpParser\Node\Name && $function_id) {
349
                $stmt_type = self::getFunctionCallReturnType(
350
                    $statements_analyzer,
351
                    $codebase,
352
                    $stmt,
353
                    $function_name,
354
                    $function_id,
355
                    $in_call_map,
356
                    $is_stubbed,
357
                    $function_storage,
358
                    $template_result,
359
                    $context
360
                );
361
362
                $statements_analyzer->node_data->setType($real_stmt, $stmt_type);
363
364
                if ($config->after_every_function_checks) {
365
                    foreach ($config->after_every_function_checks as $plugin_fq_class_name) {
366
                        $plugin_fq_class_name::afterEveryFunctionCallAnalysis(
367
                            $stmt,
368
                            $function_id,
369
                            $context,
370
                            $statements_analyzer->getSource(),
371
                            $codebase
372
                        );
373
                    }
374
                }
375
            }
376
377
            foreach ($defined_constants as $const_name => $const_type) {
378
                $context->constants[$const_name] = clone $const_type;
379
                $context->vars_in_scope[$const_name] = clone $const_type;
380
            }
381
382
            foreach ($global_variables as $var_id => $_) {
383
                $context->vars_in_scope[$var_id] = Type::getMixed();
384
                $context->vars_possibly_in_scope[$var_id] = true;
385
            }
386
387
            if ($config->use_assert_for_type &&
388
                $function_name instanceof PhpParser\Node\Name &&
389
                $function_name->parts === ['assert'] &&
390
                isset($stmt->args[0])
391
            ) {
392
                self::processAssertFunctionEffects(
393
                    $statements_analyzer,
394
                    $codebase,
395
                    $stmt,
396
                    $stmt->args[0],
397
                    $context
398
                );
399
            }
400
        }
401
402
        if ($codebase->store_node_types
403
            && !$context->collect_initializations
404
            && !$context->collect_mutations
405
            && ($stmt_type = $statements_analyzer->node_data->getType($real_stmt))
406
        ) {
407
            $codebase->analyzer->addNodeType(
408
                $statements_analyzer->getFilePath(),
409
                $stmt,
410
                $stmt_type->getId()
411
            );
412
        }
413
414
        self::checkFunctionCallPurity(
415
            $statements_analyzer,
416
            $codebase,
417
            $stmt,
418
            $function_name,
419
            $function_id,
420
            $in_call_map,
421
            $function_storage,
422
            $context
423
        );
424
425
        if ($function_storage) {
426
            $generic_params = $template_result ? $template_result->upper_bounds : [];
427
428
            if ($function_storage->assertions && $function_name instanceof PhpParser\Node\Name) {
429
                self::applyAssertionsToContext(
430
                    $function_name,
431
                    null,
432
                    $function_storage->assertions,
433
                    $stmt->args,
434
                    $generic_params,
435
                    $context,
436
                    $statements_analyzer
437
                );
438
            }
439
440
            if ($function_storage->if_true_assertions) {
441
                $statements_analyzer->node_data->setIfTrueAssertions(
442
                    $stmt,
443
                    array_map(
444
                        function (Assertion $assertion) use ($generic_params) : Assertion {
445
                            return $assertion->getUntemplatedCopy($generic_params ?: [], null);
446
                        },
447
                        $function_storage->if_true_assertions
448
                    )
449
                );
450
            }
451
452
            if ($function_storage->if_false_assertions) {
453
                $statements_analyzer->node_data->setIfFalseAssertions(
454
                    $stmt,
455
                    array_map(
456
                        function (Assertion $assertion) use ($generic_params) : Assertion {
457
                            return $assertion->getUntemplatedCopy($generic_params ?: [], null);
458
                        },
459
                        $function_storage->if_false_assertions
460
                    )
461
                );
462
            }
463
464
            if ($function_storage->deprecated && $function_id) {
465
                if (IssueBuffer::accepts(
466
                    new DeprecatedFunction(
467
                        'The function ' . $function_id . ' has been marked as deprecated',
468
                        $code_location,
469
                        $function_id
470
                    ),
471
                    $statements_analyzer->getSuppressedIssues()
472
                )) {
473
                    // continue
474
                }
475
            }
476
        }
477
478
        if ($byref_uses) {
479
            foreach ($byref_uses as $byref_use_var => $_) {
480
                $context->vars_in_scope['$' . $byref_use_var] = Type::getMixed();
481
                $context->vars_possibly_in_scope['$' . $byref_use_var] = true;
482
            }
483
        }
484
485
        if ($function_name instanceof PhpParser\Node\Name) {
486
            self::handleNamedFunction(
487
                $statements_analyzer,
488
                $codebase,
489
                $stmt,
490
                $real_stmt,
491
                $function_name,
492
                $function_id,
493
                $context
494
            );
495
        }
496
497
        if (!$statements_analyzer->node_data->getType($real_stmt)) {
498
            $statements_analyzer->node_data->setType($real_stmt, Type::getMixed());
499
        }
500
501
        return true;
502
    }
503
504
    /**
505
     * @return  array{
0 ignored issues
show
Documentation introduced by
The doc-type array{ could not be parsed: Unknown type name "array{" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
506
     *     ?bool,
507
     *     ?PhpParser\Node\Expr|PhpParser\Node\Name,
508
     *     array<int, FunctionLikeParameter>|null,
509
     *     ?array<string, bool>
510
     * }
511
     */
512
    private static function getAnalyzeNamedExpression(
513
        StatementsAnalyzer $statements_analyzer,
514
        \Psalm\Codebase $codebase,
515
        PhpParser\Node\Expr\FuncCall $stmt,
516
        PhpParser\Node\Expr\FuncCall $real_stmt,
517
        PhpParser\Node\Expr $function_name,
518
        Context $context
519
    ) {
520
        $function_params = null;
521
522
        $explicit_function_name = null;
523
        $function_exists = null;
524
        $was_in_call = $context->inside_call;
525
        $context->inside_call = true;
526
527
        if (ExpressionAnalyzer::analyze($statements_analyzer, $function_name, $context) === false) {
528
            $context->inside_call = $was_in_call;
529
530
            return [false, null, null, null];
531
        }
532
533
        $context->inside_call = $was_in_call;
534
535
        $byref_uses = [];
536
537
        if ($stmt_name_type = $statements_analyzer->node_data->getType($function_name)) {
538
            if ($stmt_name_type->isNull()) {
539
                if (IssueBuffer::accepts(
540
                    new NullFunctionCall(
541
                        'Cannot call function on null value',
542
                        new CodeLocation($statements_analyzer->getSource(), $stmt)
543
                    ),
544
                    $statements_analyzer->getSuppressedIssues()
545
                )) {
546
                    // fall through
547
                }
548
549
                return [false, null, null, null];
550
            }
551
552
            if ($stmt_name_type->isNullable()) {
553
                if (IssueBuffer::accepts(
554
                    new PossiblyNullFunctionCall(
555
                        'Cannot call function on possibly null value',
556
                        new CodeLocation($statements_analyzer->getSource(), $stmt)
557
                    ),
558
                    $statements_analyzer->getSuppressedIssues()
559
                )) {
560
                    // fall through
561
                }
562
            }
563
564
            $invalid_function_call_types = [];
565
            $has_valid_function_call_type = false;
566
567
            foreach ($stmt_name_type->getAtomicTypes() as $var_type_part) {
568
                if ($var_type_part instanceof Type\Atomic\TFn || $var_type_part instanceof Type\Atomic\TCallable) {
569
                    $function_params = $var_type_part->params;
570
571
                    if (($stmt_type = $statements_analyzer->node_data->getType($real_stmt))
572
                        && $var_type_part->return_type
573
                    ) {
574
                        $statements_analyzer->node_data->setType(
575
                            $real_stmt,
576
                            Type::combineUnionTypes(
577
                                $stmt_type,
578
                                $var_type_part->return_type
579
                            )
580
                        );
581
                    } else {
582
                        $statements_analyzer->node_data->setType(
583
                            $real_stmt,
584
                            $var_type_part->return_type ?: Type::getMixed()
585
                        );
586
                    }
587
588
                    if ($var_type_part instanceof Type\Atomic\TFn) {
589
                        $byref_uses += $var_type_part->byref_uses;
590
                    }
591
592
                    $function_exists = true;
593
                    $has_valid_function_call_type = true;
594
                } elseif ($var_type_part instanceof TTemplateParam && $var_type_part->as->hasCallableType()) {
595
                    $has_valid_function_call_type = true;
596
                } elseif ($var_type_part instanceof TMixed || $var_type_part instanceof TTemplateParam) {
597
                    $has_valid_function_call_type = true;
598
599
                    if (IssueBuffer::accepts(
600
                        new MixedFunctionCall(
601
                            'Cannot call function on ' . $var_type_part->getId(),
602
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
603
                        ),
604
                        $statements_analyzer->getSuppressedIssues()
605
                    )) {
606
                        // fall through
607
                    }
608
                } elseif ($var_type_part instanceof TCallableObject
609
                    || $var_type_part instanceof TCallableString
610
                ) {
611
                    // this is fine
612
                    $has_valid_function_call_type = true;
613
                } elseif (($var_type_part instanceof TNamedObject && $var_type_part->value === 'Closure')) {
614
                    // this is fine
615
                    $has_valid_function_call_type = true;
616
                } elseif ($var_type_part instanceof TString
617
                    || $var_type_part instanceof Type\Atomic\TArray
618
                    || $var_type_part instanceof Type\Atomic\TList
619
                    || ($var_type_part instanceof Type\Atomic\ObjectLike
620
                        && count($var_type_part->properties) === 2)
621
                ) {
622
                    $potential_method_id = null;
623
624
                    if ($var_type_part instanceof Type\Atomic\ObjectLike) {
625
                        $potential_method_id = TypeAnalyzer::getCallableMethodIdFromObjectLike(
626
                            $var_type_part,
627
                            $codebase,
628
                            $context->calling_method_id,
629
                            $statements_analyzer->getFilePath()
630
                        );
631
632
                        if ($potential_method_id === 'not-callable') {
633
                            $potential_method_id = null;
634
                        }
635
                    } elseif ($var_type_part instanceof Type\Atomic\TLiteralString) {
636
                        if (!$var_type_part->value) {
637
                            $invalid_function_call_types[] = '\'\'';
638
                            continue;
639
                        }
640
641
                        if (strpos($var_type_part->value, '::')) {
642
                            $parts = explode('::', strtolower($var_type_part->value));
643
                            $fq_class_name = $parts[0];
644
                            $fq_class_name = \preg_replace('/^\\\\/', '', $fq_class_name);
645
                            $potential_method_id = new \Psalm\Internal\MethodIdentifier($fq_class_name, $parts[1]);
646
                        } else {
647
                            $explicit_function_name = new PhpParser\Node\Name\FullyQualified(
648
                                $var_type_part->value,
649
                                $function_name->getAttributes()
650
                            );
651
                        }
652
                    }
653
654
                    if ($potential_method_id) {
655
                        $codebase->methods->methodExists(
656
                            $potential_method_id,
0 ignored issues
show
Bug introduced by
It seems like $potential_method_id can also be of type string; however, Psalm\Internal\Codebase\Methods::methodExists() does only seem to accept object<Psalm\Internal\MethodIdentifier>, 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...
657
                            $context->calling_method_id,
658
                            null,
659
                            $statements_analyzer,
660
                            $statements_analyzer->getFilePath()
661
                        );
662
                    }
663
664
                    // this is also kind of fine
665
                    $has_valid_function_call_type = true;
666
                } elseif ($var_type_part instanceof TNull) {
667
                    // handled above
668
                } elseif (!$var_type_part instanceof TNamedObject
669
                    || !$codebase->classlikes->classOrInterfaceExists($var_type_part->value)
670
                    || !$codebase->methods->methodExists(
671
                        new \Psalm\Internal\MethodIdentifier(
672
                            $var_type_part->value,
673
                            '__invoke'
674
                        )
675
                    )
676
                ) {
677
                    $invalid_function_call_types[] = (string)$var_type_part;
678
                } else {
679
                    self::analyzeInvokeCall(
680
                        $statements_analyzer,
681
                        $stmt,
682
                        $real_stmt,
683
                        $function_name,
684
                        $context
685
                    );
686
                }
687
            }
688
689
            if ($invalid_function_call_types) {
690
                $var_type_part = reset($invalid_function_call_types);
691
692
                if ($has_valid_function_call_type) {
693
                    if (IssueBuffer::accepts(
694
                        new PossiblyInvalidFunctionCall(
695
                            'Cannot treat type ' . $var_type_part . ' as callable',
696
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
697
                        ),
698
                        $statements_analyzer->getSuppressedIssues()
699
                    )) {
700
                        // fall through
701
                    }
702
                } else {
703
                    if (IssueBuffer::accepts(
704
                        new InvalidFunctionCall(
705
                            'Cannot treat type ' . $var_type_part . ' as callable',
706
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
707
                        ),
708
                        $statements_analyzer->getSuppressedIssues()
709
                    )) {
710
                        // fall through
711
                    }
712
                }
713
714
                return [false, null, null, null];
715
            }
716
        }
717
718
        if (!$statements_analyzer->node_data->getType($real_stmt)) {
719
            $statements_analyzer->node_data->setType($real_stmt, Type::getMixed());
720
        }
721
722
        return [
723
            $function_exists,
724
            $explicit_function_name ?: $function_name,
725
            $function_params,
726
            $byref_uses
727
        ];
728
    }
729
730
    private static function analyzeInvokeCall(
731
        StatementsAnalyzer $statements_analyzer,
732
        PhpParser\Node\Expr\FuncCall $stmt,
733
        PhpParser\Node\Expr\FuncCall $real_stmt,
734
        PhpParser\Node\Expr $function_name,
735
        Context $context
736
    ) : void {
737
        $old_data_provider = $statements_analyzer->node_data;
738
739
        $statements_analyzer->node_data = clone $statements_analyzer->node_data;
740
741
        $fake_method_call = new PhpParser\Node\Expr\MethodCall(
742
            $function_name,
743
            new PhpParser\Node\Identifier('__invoke', $function_name->getAttributes()),
744
            $stmt->args
745
        );
746
747
        $suppressed_issues = $statements_analyzer->getSuppressedIssues();
748
749
        if (!in_array('PossiblyNullReference', $suppressed_issues, true)) {
750
            $statements_analyzer->addSuppressedIssues(['PossiblyNullReference']);
751
        }
752
753
        if (!in_array('InternalMethod', $suppressed_issues, true)) {
754
            $statements_analyzer->addSuppressedIssues(['InternalMethod']);
755
        }
756
757
        if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
758
            $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
759
        }
760
761
        \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
762
            $statements_analyzer,
763
            $fake_method_call,
764
            $context,
765
            false
766
        );
767
768
        if (!in_array('PossiblyNullReference', $suppressed_issues, true)) {
769
            $statements_analyzer->removeSuppressedIssues(['PossiblyNullReference']);
770
        }
771
772
        if (!in_array('InternalMethod', $suppressed_issues, true)) {
773
            $statements_analyzer->removeSuppressedIssues(['InternalMethod']);
774
        }
775
776
        if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
777
            $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
778
        }
779
780
        $fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call);
781
782
        $statements_analyzer->node_data = $old_data_provider;
783
784
        if ($stmt_type = $statements_analyzer->node_data->getType($real_stmt)) {
785
            $statements_analyzer->node_data->setType(
786
                $real_stmt,
787
                Type::combineUnionTypes(
788
                    $fake_method_call_type ?: Type::getMixed(),
789
                    $stmt_type
790
                )
791
            );
792
        } else {
793
            $statements_analyzer->node_data->setType(
794
                $real_stmt,
795
                $fake_method_call_type ?: Type::getMixed()
796
            );
797
        }
798
    }
799
800
    private static function processAssertFunctionEffects(
801
        StatementsAnalyzer $statements_analyzer,
802
        \Psalm\Codebase $codebase,
803
        PhpParser\Node\Expr\FuncCall $stmt,
804
        PhpParser\Node\Arg $first_arg,
805
        Context $context
806
    ) : void {
807
        $assert_clauses = \Psalm\Type\Algebra::getFormula(
808
            \spl_object_id($first_arg->value),
809
            $first_arg->value,
810
            $context->self,
811
            $statements_analyzer,
812
            $codebase
813
        );
814
815
        $cond_assigned_var_ids = [];
816
817
        \Psalm\Internal\Analyzer\AlgebraAnalyzer::checkForParadox(
818
            $context->clauses,
819
            $assert_clauses,
820
            $statements_analyzer,
821
            $stmt,
822
            $cond_assigned_var_ids
823
        );
824
825
        $simplified_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $assert_clauses));
826
827
        $assert_type_assertions = Algebra::getTruthsFromFormula($simplified_clauses);
828
829
        if ($assert_type_assertions) {
830
            $changed_var_ids = [];
831
832
            // while in an and, we allow scope to boil over to support
833
            // statements of the form if ($x && $x->foo())
834
            $op_vars_in_scope = Reconciler::reconcileKeyedTypes(
835
                $assert_type_assertions,
836
                $assert_type_assertions,
837
                $context->vars_in_scope,
838
                $changed_var_ids,
839
                array_map(
840
                    function ($v) {
0 ignored issues
show
Unused Code introduced by
The parameter $v is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
841
                        return true;
842
                    },
843
                    $assert_type_assertions
844
                ),
845
                $statements_analyzer,
846
                $statements_analyzer->getTemplateTypeMap() ?: [],
847
                $context->inside_loop,
848
                new CodeLocation($statements_analyzer->getSource(), $stmt)
849
            );
850
851
            foreach ($changed_var_ids as $var_id => $_) {
852
                $first_appearance = $statements_analyzer->getFirstAppearance($var_id);
853
854
                if ($first_appearance
855
                    && isset($context->vars_in_scope[$var_id])
856
                    && $context->vars_in_scope[$var_id]->hasMixed()
857
                ) {
858
                    if (!$context->collect_initializations
859
                        && !$context->collect_mutations
860
                        && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
861
                        && (!(($parent_source = $statements_analyzer->getSource())
862
                                    instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
863
                                || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
864
                    ) {
865
                        $codebase->analyzer->decrementMixedCount($statements_analyzer->getFilePath());
866
                    }
867
868
                    IssueBuffer::remove(
869
                        $statements_analyzer->getFilePath(),
870
                        'MixedAssignment',
871
                        $first_appearance->raw_file_start
872
                    );
873
                }
874
875
                if (isset($op_vars_in_scope[$var_id])) {
876
                    $op_vars_in_scope[$var_id]->from_docblock = true;
877
                }
878
            }
879
880
            $context->vars_in_scope = $op_vars_in_scope;
881
        }
882
    }
883
884
    /**
885
     * @param non-empty-string $function_id
0 ignored issues
show
Documentation introduced by
The doc-type non-empty-string could not be parsed: Unknown type name "non-empty-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
886
     */
887
    private static function getFunctionCallReturnType(
888
        StatementsAnalyzer $statements_analyzer,
889
        \Psalm\Codebase $codebase,
890
        PhpParser\Node\Expr\FuncCall $stmt,
891
        PhpParser\Node\Name $function_name,
892
        string $function_id,
893
        bool $in_call_map,
894
        bool $is_stubbed,
895
        ?FunctionLikeStorage $function_storage,
896
        TemplateResult $template_result,
897
        Context $context
898
    ) : Type\Union {
899
        $stmt_type = null;
900
        $config = $codebase->config;
901
902
        if ($codebase->functions->return_type_provider->has($function_id)) {
903
            $stmt_type = $codebase->functions->return_type_provider->getReturnType(
904
                $statements_analyzer,
905
                $function_id,
906
                $stmt->args,
907
                $context,
908
                new CodeLocation($statements_analyzer->getSource(), $function_name)
909
            );
910
        }
911
912
        if (!$stmt_type) {
913
            if (!$in_call_map || $is_stubbed) {
914
                if ($function_storage && $function_storage->template_types) {
915
                    foreach ($function_storage->template_types as $template_name => $_) {
916
                        if (!isset($template_result->upper_bounds[$template_name])) {
917
                            if ($template_name === 'TFunctionArgCount') {
918
                                $template_result->upper_bounds[$template_name] = [
919
                                    'fn-' . $function_id => [Type::getInt(false, count($stmt->args)), 0]
920
                                ];
921
                            } else {
922
                                $template_result->upper_bounds[$template_name] = [
923
                                    'fn-' . $function_id => [Type::getEmpty(), 0]
924
                                ];
925
                            }
926
                        }
927
                    }
928
                }
929
930
                if ($function_storage && !$context->isSuppressingExceptions($statements_analyzer)) {
931
                    $context->mergeFunctionExceptions(
932
                        $function_storage,
933
                        new CodeLocation($statements_analyzer->getSource(), $stmt)
934
                    );
935
                }
936
937
                try {
938
                    if ($function_storage && $function_storage->return_type) {
939
                        $return_type = clone $function_storage->return_type;
940
941
                        if ($template_result->upper_bounds && $function_storage->template_types) {
942
                            $return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
943
                                $codebase,
944
                                $return_type,
945
                                null,
946
                                null,
947
                                null
948
                            );
949
950
                            $return_type->replaceTemplateTypesWithArgTypes(
951
                                $template_result,
952
                                $codebase
953
                            );
954
                        }
955
956
                        $return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
957
                            $codebase,
958
                            $return_type,
959
                            null,
960
                            null,
961
                            null
962
                        );
963
964
                        $return_type_location = $function_storage->return_type_location;
965
966
                        if ($config->after_function_checks) {
967
                            $file_manipulations = [];
968
969
                            foreach ($config->after_function_checks as $plugin_fq_class_name) {
970
                                $plugin_fq_class_name::afterFunctionCallAnalysis(
971
                                    $stmt,
972
                                    $function_id,
973
                                    $context,
974
                                    $statements_analyzer->getSource(),
975
                                    $codebase,
976
                                    $file_manipulations,
977
                                    $return_type
978
                                );
979
                            }
980
981
                            if ($file_manipulations) {
982
                                FileManipulationBuffer::add(
983
                                    $statements_analyzer->getFilePath(),
984
                                    $file_manipulations
985
                                );
986
                            }
987
                        }
988
989
                        if ($return_type === null) {
990
                            throw new \UnexpectedValueException('$return_type shouldn’t be null here');
991
                        }
992
993
                        $stmt_type = $return_type;
994
                        $return_type->by_ref = $function_storage->returns_by_ref;
995
996
                        // only check the type locally if it's defined externally
997
                        if ($return_type_location &&
998
                            !$is_stubbed && // makes lookups or array_* functions quicker
999
                            !$config->isInProjectDirs($return_type_location->file_path)
1000
                        ) {
1001
                            $return_type->check(
1002
                                $statements_analyzer,
1003
                                new CodeLocation($statements_analyzer->getSource(), $stmt),
1004
                                $statements_analyzer->getSuppressedIssues(),
1005
                                $context->phantom_classes,
1006
                                true,
1007
                                false,
1008
                                false,
1009
                                $context->calling_method_id
1010
                            );
1011
                        }
1012
                    }
1013
                } catch (\InvalidArgumentException $e) {
1014
                    // this can happen when the function was defined in the Config startup script
1015
                    $stmt_type = Type::getMixed();
1016
                }
1017
            } else {
1018
                $stmt_type = FunctionAnalyzer::getReturnTypeFromCallMapWithArgs(
1019
                    $statements_analyzer,
1020
                    $function_id,
1021
                    $stmt->args,
1022
                    $context
1023
                );
1024
            }
1025
        }
1026
1027
        if (!$stmt_type) {
1028
            $stmt_type = Type::getMixed();
1029
        }
1030
1031
        if ($function_storage) {
1032
            self::taintReturnType($statements_analyzer, $stmt, $function_id, $function_storage, $stmt_type);
1033
        }
1034
1035
1036
        return $stmt_type;
1037
    }
1038
1039
    private static function taintReturnType(
1040
        StatementsAnalyzer $statements_analyzer,
1041
        PhpParser\Node\Expr\FuncCall $stmt,
1042
        string $function_id,
1043
        FunctionLikeStorage $function_storage,
1044
        Type\Union $stmt_type
1045
    ) : void {
1046
        $codebase = $statements_analyzer->getCodebase();
1047
1048
        if (!$codebase->taint
1049
            || !$codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())
1050
            || \in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
1051
        ) {
1052
            return;
1053
        }
1054
1055
        $return_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
1056
1057
        $function_return_sink = TaintNode::getForMethodReturn(
1058
            $function_id,
1059
            $function_id,
1060
            $return_location,
1061
            $function_storage->specialize_call ? $return_location : null
1062
        );
1063
1064
        $codebase->taint->addTaintNode($function_return_sink);
1065
1066
        $stmt_type->parent_nodes[] = $function_return_sink;
1067
1068
        if ($function_storage->return_source_params) {
1069
            $removed_taints = $function_storage->removed_taints;
1070
1071
            if ($function_id === 'preg_replace' && count($stmt->args) > 2) {
1072
                $first_stmt_type = $statements_analyzer->node_data->getType($stmt->args[0]->value);
1073
                $second_stmt_type = $statements_analyzer->node_data->getType($stmt->args[1]->value);
1074
1075
                if ($first_stmt_type
1076
                    && $second_stmt_type
1077
                    && $first_stmt_type->isSingleStringLiteral()
1078
                    && $second_stmt_type->isSingleStringLiteral()
1079
                ) {
1080
                    $first_arg_value = $first_stmt_type->getSingleStringLiteral()->value;
1081
1082
                    $pattern = \substr($first_arg_value, 1, -1);
1083
1084
                    if ($pattern[0] === '['
1085
                        && $pattern[1] === '^'
1086
                        && \substr($pattern, -1) === ']'
1087
                    ) {
1088
                        $pattern = \substr($pattern, 2, -1);
1089
1090
                        if (self::simpleExclusion($pattern, $first_arg_value[0])) {
1091
                            $removed_taints[] = 'html';
1092
                            $removed_taints[] = 'sql';
1093
                        }
1094
                    }
1095
                }
1096
            }
1097
1098
            foreach ($function_storage->return_source_params as $i => $path_type) {
1099
                if (!isset($stmt->args[$i])) {
1100
                    continue;
1101
                }
1102
1103
                $arg_location = new CodeLocation(
1104
                    $statements_analyzer->getSource(),
1105
                    $stmt->args[$i]->value
1106
                );
1107
1108
                $function_param_sink = TaintNode::getForMethodArgument(
1109
                    $function_id,
1110
                    $function_id,
1111
                    $i,
1112
                    $arg_location,
1113
                    $function_storage->specialize_call ? $return_location : null
1114
                );
1115
1116
                $codebase->taint->addTaintNode($function_param_sink);
1117
1118
                $codebase->taint->addPath(
1119
                    $function_param_sink,
1120
                    $function_return_sink,
1121
                    $path_type,
1122
                    $function_storage->added_taints,
1123
                    $removed_taints
1124
                );
1125
            }
1126
        }
1127
1128
        if ($function_storage->taint_source_types) {
1129
            $method_node = Source::getForMethodReturn(
1130
                $function_id,
1131
                $function_id,
1132
                $return_location
1133
            );
1134
1135
            $method_node->taints = $function_storage->taint_source_types;
1136
1137
            $codebase->taint->addSource($method_node);
1138
        }
1139
    }
1140
1141
    private static function simpleExclusion(string $pattern, string $escape_char) : bool
1142
    {
1143
        $str_length = \strlen($pattern);
1144
1145
        for ($i = 0; $i < $str_length; $i++) {
1146
            $current = $pattern[$i];
1147
            $next = $pattern[$i + 1] ?? null;
1148
1149
            if ($current === '\\') {
1150
                if ($next == null
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $next of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1151
                    || $next === 'x'
1152
                    || $next === 'u'
1153
                ) {
1154
                    return false;
1155
                }
1156
1157
                if ($next === '.'
1158
                    || $next === '('
1159
                    || $next === ')'
1160
                    || $next === '['
1161
                    || $next === ']'
1162
                    || $next === 's'
1163
                    || $next === 'w'
1164
                    || $next === $escape_char
1165
                ) {
1166
                    $i++;
1167
                    continue;
1168
                }
1169
1170
                return false;
1171
            }
1172
1173
            if ($next !== '-') {
1174
                if ($current === '_'
1175
                    || $current === '-'
1176
                    || $current === '|'
1177
                    || $current === ':'
1178
                    || $current === '#'
1179
                    || $current === '.'
1180
                    || $current === ' '
1181
                ) {
1182
                    continue;
1183
                }
1184
1185
                return false;
1186
            }
1187
1188
            if ($current === ']') {
1189
                return false;
1190
            }
1191
1192
            if (!isset($pattern[$i + 2]) || $next !== '-') {
1193
                return false;
1194
            }
1195
1196
            if (($current === 'a' && $pattern[$i + 2] === 'z')
1197
                || ($current === 'a' && $pattern[$i + 2] === 'Z')
1198
                || ($current === 'A' && $pattern[$i + 2] === 'Z')
1199
                || ($current === '0' && $pattern[$i + 2] === '9')
1200
            ) {
1201
                $i += 2;
1202
                continue;
1203
            }
1204
1205
            return false;
1206
        }
1207
1208
        return true;
1209
    }
1210
1211
    private static function checkFunctionCallPurity(
1212
        StatementsAnalyzer $statements_analyzer,
1213
        \Psalm\Codebase $codebase,
1214
        PhpParser\Node\Expr\FuncCall $stmt,
1215
        PhpParser\Node $function_name,
1216
        ?string $function_id,
1217
        bool $in_call_map,
1218
        ?FunctionLikeStorage $function_storage,
1219
        Context $context
1220
    ) : void {
1221
        $config = $codebase->config;
1222
1223
        if (!$context->collect_initializations
1224
            && !$context->collect_mutations
1225
            && ($context->mutation_free
1226
                || $context->external_mutation_free
1227
                || $codebase->find_unused_variables
1228
                || !$config->remember_property_assignments_after_call)
1229
        ) {
1230
            $must_use = true;
1231
1232
            $callmap_function_pure = $function_id && $in_call_map
1233
                ? $codebase->functions->isCallMapFunctionPure(
1234
                    $codebase,
1235
                    $statements_analyzer->node_data,
1236
                    $function_id,
1237
                    $stmt->args,
1238
                    $must_use
1239
                )
1240
                : null;
1241
1242
            if ((!$in_call_map
1243
                    && $function_storage
1244
                    && !$function_storage->pure)
1245
                || ($callmap_function_pure === false)
1246
            ) {
1247
                if ($context->mutation_free || $context->external_mutation_free) {
1248
                    if (IssueBuffer::accepts(
1249
                        new ImpureFunctionCall(
1250
                            'Cannot call an impure function from a mutation-free context',
1251
                            new CodeLocation($statements_analyzer, $function_name)
1252
                        ),
1253
                        $statements_analyzer->getSuppressedIssues()
1254
                    )) {
1255
                        // fall through
1256
                    }
1257
                }
1258
1259
                if (!$config->remember_property_assignments_after_call) {
1260
                    $context->removeAllObjectVars();
1261
                }
1262
            } elseif ($function_id
1263
                && (($function_storage
1264
                        && $function_storage->pure
1265
                        && !$function_storage->assertions
1266
                        && $must_use)
1267
                    || ($callmap_function_pure === true && $must_use))
1268
                && $codebase->find_unused_variables
1269
                && !$context->inside_conditional
1270
                && !$context->inside_unset
1271
            ) {
1272
                if (!$context->inside_assignment && !$context->inside_call) {
1273
                    if (IssueBuffer::accepts(
1274
                        new UnusedFunctionCall(
1275
                            'The call to ' . $function_id . ' is not used',
1276
                            new CodeLocation($statements_analyzer, $function_name),
1277
                            $function_id
1278
                        ),
1279
                        $statements_analyzer->getSuppressedIssues()
1280
                    )) {
1281
                        // fall through
1282
                    }
1283
                } else {
1284
                    /** @psalm-suppress UndefinedPropertyAssignment */
1285
                    $stmt->pure = true;
1286
                }
1287
            }
1288
        }
1289
    }
1290
1291
    private static function handleNamedFunction(
1292
        StatementsAnalyzer $statements_analyzer,
1293
        \Psalm\Codebase $codebase,
1294
        PhpParser\Node\Expr\FuncCall $stmt,
1295
        PhpParser\Node\Expr\FuncCall $real_stmt,
1296
        PhpParser\Node\Name $function_name,
1297
        ?string $function_id,
1298
        Context $context
1299
    ) : void {
1300
        $first_arg = isset($stmt->args[0]) ? $stmt->args[0] : null;
1301
1302
        if ($function_name->parts === ['get_class'] || $function_name->parts === ['gettype']) {
1303
            if ($first_arg) {
1304
                $var = $first_arg->value;
1305
1306
                if ($var instanceof PhpParser\Node\Expr\Variable
1307
                    && is_string($var->name)
1308
                ) {
1309
                    $var_id = '$' . $var->name;
1310
1311
                    if (isset($context->vars_in_scope[$var_id])) {
1312
                        $atomic_type = $function_name->parts === ['get_class']
1313
                            ? new Type\Atomic\GetClassT(
1314
                                $var_id,
1315
                                $context->vars_in_scope[$var_id]->hasMixed()
1316
                                    ? Type::getObject()
1317
                                    : $context->vars_in_scope[$var_id]
1318
                            )
1319
                            : new Type\Atomic\GetTypeT($var_id);
1320
1321
                        $statements_analyzer->node_data->setType($real_stmt, new Type\Union([$atomic_type]));
1322
                    }
1323
                } elseif ($var_type = $statements_analyzer->node_data->getType($var)) {
1324
                    $class_string_types = [];
1325
1326
                    foreach ($var_type->getAtomicTypes() as $class_type) {
1327
                        if ($class_type instanceof Type\Atomic\TNamedObject) {
1328
                            $class_string_types[] = new Type\Atomic\TClassString($class_type->value, clone $class_type);
1329
                        } elseif ($class_type instanceof Type\Atomic\TTemplateParam
1330
                            && $class_type->as->isSingle()
1331
                        ) {
1332
                            $as_atomic_type = \array_values($class_type->as->getAtomicTypes())[0];
1333
1334
                            if ($as_atomic_type instanceof Type\Atomic\TObject) {
1335
                                $class_string_types[] = new Type\Atomic\TTemplateParamClass(
1336
                                    $class_type->param_name,
1337
                                    'object',
1338
                                    null,
1339
                                    $class_type->defining_class
1340
                                );
1341
                            } elseif ($as_atomic_type instanceof TNamedObject) {
1342
                                $class_string_types[] = new Type\Atomic\TTemplateParamClass(
1343
                                    $class_type->param_name,
1344
                                    $as_atomic_type->value,
1345
                                    $as_atomic_type,
1346
                                    $class_type->defining_class
1347
                                );
1348
                            }
1349
                        } else {
1350
                            $class_string_types[] = new Type\Atomic\TClassString();
1351
                        }
1352
                    }
1353
1354
                    if ($class_string_types) {
1355
                        $statements_analyzer->node_data->setType($real_stmt, new Type\Union($class_string_types));
1356
                    }
1357
                }
1358
            } elseif ($function_name->parts === ['get_class']
1359
                && ($get_class_name = $statements_analyzer->getFQCLN())
1360
            ) {
1361
                $statements_analyzer->node_data->setType(
1362
                    $real_stmt,
1363
                    new Type\Union([
1364
                        new Type\Atomic\TClassString(
1365
                            $get_class_name,
1366
                            new Type\Atomic\TNamedObject($get_class_name)
1367
                        )
1368
                    ])
1369
                );
1370
            }
1371
        }
1372
1373
        if ($function_name->parts === ['method_exists']) {
1374
            $second_arg = isset($stmt->args[1]) ? $stmt->args[1] : null;
1375
1376
            if ($first_arg
1377
                && $first_arg->value instanceof PhpParser\Node\Expr\Variable
1378
                && $second_arg
1379
                && $second_arg->value instanceof PhpParser\Node\Scalar\String_
1380
            ) {
1381
                // do nothing
1382
            } else {
1383
                $context->check_methods = false;
1384
            }
1385
        } elseif ($function_name->parts === ['class_exists']) {
1386
            if ($first_arg) {
1387
                if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
1388
                    if (!$codebase->classlikes->classExists($first_arg->value->value)) {
1389
                        $context->phantom_classes[strtolower($first_arg->value->value)] = true;
1390
                    }
1391
                } elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
1392
                    && $first_arg->value->class instanceof PhpParser\Node\Name
1393
                    && $first_arg->value->name instanceof PhpParser\Node\Identifier
1394
                    && $first_arg->value->name->name === 'class'
1395
                ) {
1396
                    $resolved_name = (string) $first_arg->value->class->getAttribute('resolvedName');
1397
1398
                    if (!$codebase->classlikes->classExists($resolved_name)) {
1399
                        $context->phantom_classes[strtolower($resolved_name)] = true;
1400
                    }
1401
                }
1402
            }
1403
        } elseif ($function_name->parts === ['interface_exists']) {
1404
            if ($first_arg) {
1405
                if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
1406
                    $context->phantom_classes[strtolower($first_arg->value->value)] = true;
1407
                } elseif ($first_arg->value instanceof PhpParser\Node\Expr\ClassConstFetch
1408
                    && $first_arg->value->class instanceof PhpParser\Node\Name
1409
                    && $first_arg->value->name instanceof PhpParser\Node\Identifier
1410
                    && $first_arg->value->name->name === 'class'
1411
                ) {
1412
                    $resolved_name = (string) $first_arg->value->class->getAttribute('resolvedName');
1413
1414
                    if (!$codebase->classlikes->interfaceExists($resolved_name)) {
1415
                        $context->phantom_classes[strtolower($resolved_name)] = true;
1416
                    }
1417
                }
1418
            }
1419
        } elseif ($function_name->parts === ['file_exists'] && $first_arg) {
1420
            $var_id = ExpressionIdentifier::getArrayVarId($first_arg->value, null);
1421
1422
            if ($var_id) {
1423
                $context->phantom_files[$var_id] = true;
1424
            }
1425
        } elseif ($function_name->parts === ['extension_loaded']) {
1426
            if ($first_arg
1427
                && $first_arg->value instanceof PhpParser\Node\Scalar\String_
1428
            ) {
1429
                if (@extension_loaded($first_arg->value->value)) {
1430
                    // do nothing
1431
                } else {
1432
                    $context->check_classes = false;
1433
                }
1434
            }
1435
        } elseif ($function_name->parts === ['function_exists']) {
1436
            $context->check_functions = false;
1437
        } elseif ($function_name->parts === ['is_callable']) {
1438
            $context->check_methods = false;
1439
            $context->check_functions = false;
1440
        } elseif ($function_name->parts === ['defined']) {
1441
            $context->check_consts = false;
1442
        } elseif ($function_name->parts === ['extract']) {
1443
            $context->check_variables = false;
1444
        } elseif (strtolower($function_name->parts[0]) === 'var_dump'
1445
            || strtolower($function_name->parts[0]) === 'shell_exec') {
1446
            if (IssueBuffer::accepts(
1447
                new ForbiddenCode(
1448
                    'Unsafe ' . implode('', $function_name->parts),
1449
                    new CodeLocation($statements_analyzer->getSource(), $stmt)
1450
                ),
1451
                $statements_analyzer->getSuppressedIssues()
1452
            )) {
1453
                // continue
1454
            }
1455
        } elseif (isset($codebase->config->forbidden_functions[strtolower((string) $function_name)])) {
1456
            if (IssueBuffer::accepts(
1457
                new ForbiddenCode(
1458
                    'You have forbidden the use of ' . $function_name,
1459
                    new CodeLocation($statements_analyzer->getSource(), $stmt)
1460
                ),
1461
                $statements_analyzer->getSuppressedIssues()
1462
            )) {
1463
                // continue
1464
            }
1465
        } elseif ($function_name->parts === ['define']) {
1466
            if ($first_arg) {
1467
                $fq_const_name = ConstFetchAnalyzer::getConstName(
1468
                    $first_arg->value,
1469
                    $statements_analyzer->node_data,
1470
                    $codebase,
1471
                    $statements_analyzer->getAliases()
1472
                );
1473
1474
                if ($fq_const_name !== null && isset($stmt->args[1])) {
1475
                    $second_arg = $stmt->args[1];
1476
                    $was_in_call = $context->inside_call;
1477
                    $context->inside_call = true;
1478
                    ExpressionAnalyzer::analyze($statements_analyzer, $second_arg->value, $context);
1479
                    $context->inside_call = $was_in_call;
1480
1481
                    ConstFetchAnalyzer::setConstType(
1482
                        $statements_analyzer,
1483
                        $fq_const_name,
1484
                        $statements_analyzer->node_data->getType($second_arg->value) ?: Type::getMixed(),
1485
                        $context
1486
                    );
1487
                }
1488
            } else {
1489
                $context->check_consts = false;
1490
            }
1491
        } elseif ($function_name->parts === ['constant']) {
1492
            if ($first_arg) {
1493
                $fq_const_name = ConstFetchAnalyzer::getConstName(
1494
                    $first_arg->value,
1495
                    $statements_analyzer->node_data,
1496
                    $codebase,
1497
                    $statements_analyzer->getAliases()
1498
                );
1499
1500
                if ($fq_const_name !== null) {
1501
                    $const_type = ConstFetchAnalyzer::getConstType(
1502
                        $statements_analyzer,
1503
                        $fq_const_name,
1504
                        true,
1505
                        $context
1506
                    );
1507
1508
                    if ($const_type) {
1509
                        $statements_analyzer->node_data->setType($real_stmt, $const_type);
1510
                    }
1511
                }
1512
            } else {
1513
                $context->check_consts = false;
1514
            }
1515
        } elseif ($first_arg
1516
            && $function_id
1517
            && strpos($function_id, 'is_') === 0
1518
            && $function_id !== 'is_a'
1519
        ) {
1520
            $stmt_assertions = $statements_analyzer->node_data->getAssertions($stmt);
1521
1522
            if ($stmt_assertions !== null) {
1523
                $assertions = $stmt_assertions;
1524
            } else {
1525
                $assertions = AssertionFinder::processFunctionCall(
1526
                    $stmt,
1527
                    $context->self,
1528
                    $statements_analyzer,
1529
                    $codebase,
1530
                    $context->inside_negation
1531
                );
1532
            }
1533
1534
            $changed_vars = [];
1535
1536
            $referenced_var_ids = array_map(
1537
                function (array $_) : bool {
0 ignored issues
show
Unused Code introduced by
The parameter $_ is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1538
                    return true;
1539
                },
1540
                $assertions
1541
            );
1542
1543
            if ($assertions) {
1544
                Reconciler::reconcileKeyedTypes(
1545
                    $assertions,
1546
                    $assertions,
1547
                    $context->vars_in_scope,
1548
                    $changed_vars,
1549
                    $referenced_var_ids,
1550
                    $statements_analyzer,
1551
                    [],
1552
                    $context->inside_loop,
1553
                    new CodeLocation($statements_analyzer->getSource(), $stmt)
1554
                );
1555
            }
1556
        } elseif ($first_arg && $function_id === 'strtolower') {
1557
            $first_arg_type = $statements_analyzer->node_data->getType($first_arg->value);
1558
1559
            if ($first_arg_type
1560
                && TypeAnalyzer::isContainedBy(
1561
                    $codebase,
1562
                    $first_arg_type,
1563
                    new Type\Union([new Type\Atomic\TLowercaseString()])
1564
                )
1565
            ) {
1566
                if ($first_arg_type->from_docblock) {
1567
                    if (IssueBuffer::accepts(
1568
                        new \Psalm\Issue\RedundantConditionGivenDocblockType(
1569
                            'The call to strtolower is unnecessary given the docblock type',
1570
                            new CodeLocation($statements_analyzer, $function_name)
1571
                        ),
1572
                        $statements_analyzer->getSuppressedIssues()
1573
                    )) {
1574
                        // fall through
1575
                    }
1576
                } else {
1577
                    if (IssueBuffer::accepts(
1578
                        new \Psalm\Issue\RedundantCondition(
1579
                            'The call to strtolower is unnecessary',
1580
                            new CodeLocation($statements_analyzer, $function_name)
1581
                        ),
1582
                        $statements_analyzer->getSuppressedIssues()
1583
                    )) {
1584
                        // fall through
1585
                    }
1586
                }
1587
            }
1588
        }
1589
    }
1590
}
1591