FunctionCallAnalyzer::checkFunctionCallPurity()   D
last analyzed

Complexity

Conditions 30
Paths 41

Size

Total Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 30
nc 41
nop 8
dl 0
loc 79
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
3
4
use PhpParser;
5
use Psalm\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