Passed
Pull Request — master (#861)
by SignpostMarv
06:33 queued 02:07
created

getTranslatedFunctionParam()   D

Complexity

Conditions 16
Paths 216

Size

Total Lines 71

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 216
nop 1
dl 0
loc 71
rs 4.5333
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\Visitor;
3
4
use PhpParser;
5
use Psalm\Aliases;
6
use Psalm\Checker\ClassChecker;
7
use Psalm\Checker\ClassLikeChecker;
8
use Psalm\Checker\CommentChecker;
9
use Psalm\Checker\Statements\Expression\CallChecker;
10
use Psalm\Checker\Statements\Expression\IncludeChecker;
11
use Psalm\Checker\StatementsChecker;
12
use Psalm\Codebase;
13
use Psalm\Codebase\CallMap;
14
use Psalm\Codebase\PropertyMap;
15
use Psalm\CodeLocation;
16
use Psalm\Config;
17
use Psalm\Exception\DocblockParseException;
18
use Psalm\Exception\FileIncludeException;
19
use Psalm\Exception\IncorrectDocblockException;
20
use Psalm\Exception\TypeParseTreeException;
21
use Psalm\FileSource;
22
use Psalm\Issue\DuplicateClass;
23
use Psalm\Issue\DuplicateParam;
24
use Psalm\Issue\InvalidDocblock;
25
use Psalm\Issue\MisplacedRequiredParam;
26
use Psalm\Issue\MissingDocblockType;
27
use Psalm\IssueBuffer;
28
use Psalm\Scanner\FileScanner;
29
use Psalm\Storage\ClassLikeStorage;
30
use Psalm\Storage\FileStorage;
31
use Psalm\Storage\FunctionLikeParameter;
32
use Psalm\Storage\FunctionLikeStorage;
33
use Psalm\Storage\MethodStorage;
34
use Psalm\Storage\PropertyStorage;
35
use Psalm\Type;
36
37
class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements PhpParser\NodeVisitor, FileSource
38
{
39
    /** @var Aliases */
40
    private $aliases;
41
42
    /** @var Aliases */
43
    private $file_aliases;
44
45
    /**
46
     * @var string[]
47
     */
48
    private $fq_classlike_names = [];
49
50
    /** @var FileScanner */
51
    private $file_scanner;
52
53
    /** @var Codebase */
54
    private $codebase;
55
56
    /** @var string */
57
    private $file_path;
58
59
    /** @var bool */
60
    private $scan_deep;
61
62
    /** @var Config */
63
    private $config;
64
65
    /** @var bool */
66
    private $queue_strings_as_possible_type = false;
67
68
    /** @var array<string, string> */
69
    private $class_template_types = [];
70
71
    /** @var array<string, string> */
72
    private $function_template_types = [];
73
74
    /** @var FunctionLikeStorage[] */
75
    private $functionlike_storages = [];
76
77
    /** @var FileStorage */
78
    private $file_storage;
79
80
    /** @var ClassLikeStorage[] */
81
    private $classlike_storages = [];
82
83
    /** @var string[] */
84
    private $after_classlike_check_plugins;
85
86
    public function __construct(
87
        Codebase $codebase,
88
        FileStorage $file_storage,
89
        FileScanner $file_scanner
90
    ) {
91
        $this->codebase = $codebase;
92
        $this->file_scanner = $file_scanner;
93
        $this->file_path = $file_scanner->file_path;
94
        $this->scan_deep = $file_scanner->will_analyze;
95
        $this->config = $codebase->config;
96
        $this->aliases = $this->file_aliases = new Aliases();
97
        $this->file_storage = $file_storage;
98
        $this->after_classlike_check_plugins = $this->config->after_visit_classlikes;
99
    }
100
101
    /**
102
     * @param  PhpParser\Node $node
103
     *
104
     * @return null|int
105
     */
106
    public function enterNode(PhpParser\Node $node)
107
    {
108
        if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
109
            $this->file_aliases = $this->aliases;
110
            $this->aliases = new Aliases(
111
                $node->name ? implode('\\', $node->name->parts) : '',
112
                $this->aliases->uses,
113
                $this->aliases->functions,
114
                $this->aliases->constants
115
            );
116
        } elseif ($node instanceof PhpParser\Node\Stmt\Use_) {
117
            foreach ($node->uses as $use) {
118
                $use_path = implode('\\', $use->name->parts);
119
120
                switch ($use->type !== PhpParser\Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $node->type) {
121
                    case PhpParser\Node\Stmt\Use_::TYPE_FUNCTION:
122
                        $this->aliases->functions[strtolower($use->alias)] = $use_path;
123
                        break;
124
125
                    case PhpParser\Node\Stmt\Use_::TYPE_CONSTANT:
126
                        $this->aliases->constants[$use->alias] = $use_path;
127
                        break;
128
129
                    case PhpParser\Node\Stmt\Use_::TYPE_NORMAL:
130
                        $this->aliases->uses[strtolower($use->alias)] = $use_path;
131
                        break;
132
                }
133
            }
134
        } elseif ($node instanceof PhpParser\Node\Stmt\GroupUse) {
135
            $use_prefix = implode('\\', $node->prefix->parts);
136
137
            foreach ($node->uses as $use) {
138
                $use_path = $use_prefix . '\\' . implode('\\', $use->name->parts);
139
140
                switch ($use->type !== PhpParser\Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $node->type) {
141
                    case PhpParser\Node\Stmt\Use_::TYPE_FUNCTION:
142
                        $this->aliases->functions[strtolower($use->alias)] = $use_path;
143
                        break;
144
145
                    case PhpParser\Node\Stmt\Use_::TYPE_CONSTANT:
146
                        $this->aliases->constants[$use->alias] = $use_path;
147
                        break;
148
149
                    case PhpParser\Node\Stmt\Use_::TYPE_NORMAL:
150
                        $this->aliases->uses[strtolower($use->alias)] = $use_path;
151
                        break;
152
                }
153
            }
154
        } elseif ($node instanceof PhpParser\Node\Stmt\ClassLike) {
155
            $class_location = new CodeLocation($this->file_scanner, $node, null, true);
156
157
            $storage = null;
158
159
            if ($node->name === null) {
160
                if (!$node instanceof PhpParser\Node\Stmt\Class_) {
161
                    throw new \LogicException('Anonymous classes are always classes');
162
                }
163
164
                $fq_classlike_name = ClassChecker::getAnonymousClassName($node, $this->file_path);
165
            } else {
166
                $fq_classlike_name = ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $node->name;
167
168
                if ($this->codebase->classlike_storage_provider->has($fq_classlike_name)) {
169
                    $duplicate_storage = $this->codebase->classlike_storage_provider->get($fq_classlike_name);
170
171
                    if (!$this->codebase->register_stub_files) {
172
                        if (!$duplicate_storage->location
173
                            || $duplicate_storage->location->file_path !== $this->file_path
174
                            || $class_location->getHash() !== $duplicate_storage->location->getHash()
175
                        ) {
176
                            if (IssueBuffer::accepts(
177
                                new DuplicateClass(
178
                                    'Class ' . $fq_classlike_name . ' has already been defined'
179
                                        . ($duplicate_storage->location
180
                                            ? ' in ' . $duplicate_storage->location->file_path
181
                                            : ''),
182
                                    new CodeLocation($this->file_scanner, $node, null, true)
183
                                )
184
                            )) {
185
                                $this->file_storage->has_visitor_issues = true;
186
                            }
187
188
                            return PhpParser\NodeTraverser::STOP_TRAVERSAL;
189
                        }
190
                    } elseif ($duplicate_storage->location
191
                        && ($duplicate_storage->location->file_path !== $this->file_path
192
                            || $class_location->getHash() !== $duplicate_storage->location->getHash())
193
                    ) {
194
                        // we're overwriting some methods
195
                        $storage = $duplicate_storage;
196
                    }
197
                }
198
            }
199
200
            $fq_classlike_name_lc = strtolower($fq_classlike_name);
201
202
            $this->file_storage->classlikes_in_file[$fq_classlike_name_lc] = $fq_classlike_name;
203
204
            $this->fq_classlike_names[] = $fq_classlike_name;
205
206
            if (!$storage) {
207
                $storage = $this->codebase->createClassLikeStorage($fq_classlike_name);
208
            }
209
210
            $storage->location = $class_location;
211
            $storage->user_defined = !$this->codebase->register_stub_files;
212
            $storage->stubbed = $this->codebase->register_stub_files;
213
214
            $doc_comment = $node->getDocComment();
215
216
            $this->classlike_storages[] = $storage;
217
218
            if ($doc_comment) {
219
                $docblock_info = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $docblock_info is dead and can be removed.
Loading history...
220
                try {
221
                    $docblock_info = CommentChecker::extractClassLikeDocblockInfo(
222
                        (string)$doc_comment,
223
                        $doc_comment->getLine()
224
                    );
225
                } catch (DocblockParseException $e) {
226
                    if (IssueBuffer::accepts(
227
                        new InvalidDocblock(
228
                            $e->getMessage() . ' in docblock for ' . implode('.', $this->fq_classlike_names),
229
                            new CodeLocation($this->file_scanner, $node, null, true)
230
                        )
231
                    )) {
232
                        $storage->has_visitor_issues = true;
233
                    }
234
                }
235
236
                if ($docblock_info) {
0 ignored issues
show
introduced by
$docblock_info is of type Psalm\Scanner\ClassLikeDocblockComment, thus it always evaluated to true.
Loading history...
237
                    if ($docblock_info->template_type_names) {
238
                        $storage->template_types = [];
239
240
                        foreach ($docblock_info->template_type_names as $template_type) {
241
                            if (count($template_type) === 3) {
242
                                $storage->template_types[$template_type[0]] = Type::parseTokens(
243
                                    Type::fixUpLocalType(
244
                                        $template_type[2],
245
                                        $this->aliases
246
                                    )
247
                                );
248
                            } else {
249
                                $storage->template_types[$template_type[0]] = Type::getMixed();
250
                            }
251
                        }
252
253
                        $this->class_template_types = $storage->template_types;
254
255
                        if ($docblock_info->template_parents) {
256
                            $storage->template_parents = [];
257
258
                            foreach ($docblock_info->template_parents as $template_parent) {
259
                                $storage->template_parents[$template_parent] = $template_parent;
260
                            }
261
                        }
262
                    }
263
264
                    if ($docblock_info->properties) {
265
                        foreach ($docblock_info->properties as $property) {
266
                            $pseudo_property_type_tokens = Type::fixUpLocalType(
267
                                $property['type'],
268
                                $this->aliases
269
                            );
270
271
                            $pseudo_property_type = Type::parseTokens($pseudo_property_type_tokens);
272
                            $pseudo_property_type->setFromDocblock();
273
274
                            if ($property['tag'] !== 'property-read') {
275
                                $storage->pseudo_property_set_types[$property['name']] = $pseudo_property_type;
276
                            }
277
278
                            if ($property['tag'] !== 'property-write') {
279
                                $storage->pseudo_property_get_types[$property['name']] = $pseudo_property_type;
280
                            }
281
                        }
282
                    }
283
284
                    $storage->deprecated = $docblock_info->deprecated;
285
286
                    $storage->sealed_properties = $docblock_info->sealed_properties;
287
                    $storage->sealed_methods = $docblock_info->sealed_methods;
288
289
                    $storage->suppressed_issues = $docblock_info->suppressed_issues;
290
291
                    foreach ($docblock_info->methods as $method) {
292
                        $storage->pseudo_methods[strtolower($method->name)]
293
                            = $this->registerFunctionLike($method, true);
294
                    }
295
                }
296
            }
297
298
            if ($node instanceof PhpParser\Node\Stmt\Class_) {
299
                $storage->abstract = (bool)$node->isAbstract();
300
                $storage->final = (bool)$node->isFinal();
301
302
                $this->codebase->classlikes->addFullyQualifiedClassName($fq_classlike_name, $this->file_path);
303
304
                if ($node->extends) {
305
                    $parent_fqcln = ClassLikeChecker::getFQCLNFromNameObject($node->extends, $this->aliases);
306
                    $this->codebase->scanner->queueClassLikeForScanning(
307
                        $parent_fqcln,
308
                        $this->file_path,
309
                        $this->scan_deep
310
                    );
311
                    $parent_fqcln_lc = strtolower($parent_fqcln);
312
                    $storage->parent_classes[$parent_fqcln_lc] = $parent_fqcln_lc;
313
                    $this->file_storage->required_classes[strtolower($parent_fqcln)] = $parent_fqcln;
314
                }
315
316
                foreach ($node->implements as $interface) {
317
                    $interface_fqcln = ClassLikeChecker::getFQCLNFromNameObject($interface, $this->aliases);
318
                    $this->codebase->scanner->queueClassLikeForScanning($interface_fqcln, $this->file_path);
319
                    $storage->class_implements[strtolower($interface_fqcln)] = $interface_fqcln;
320
                    $this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
321
                }
322
            } elseif ($node instanceof PhpParser\Node\Stmt\Interface_) {
323
                $storage->is_interface = true;
324
                $this->codebase->classlikes->addFullyQualifiedInterfaceName($fq_classlike_name, $this->file_path);
325
326
                foreach ($node->extends as $interface) {
327
                    $interface_fqcln = ClassLikeChecker::getFQCLNFromNameObject($interface, $this->aliases);
328
                    $this->codebase->scanner->queueClassLikeForScanning($interface_fqcln, $this->file_path);
329
                    $storage->parent_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
330
                    $this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
331
                }
332
            } elseif ($node instanceof PhpParser\Node\Stmt\Trait_) {
333
                $storage->is_trait = true;
334
                $this->file_storage->has_trait = true;
335
                $this->codebase->classlikes->addFullyQualifiedTraitName($fq_classlike_name, $this->file_path);
336
                $this->codebase->classlikes->addTraitNode(
337
                    $fq_classlike_name,
338
                    $node,
339
                    $this->aliases
340
                );
341
            }
342
343
            foreach ($node->stmts as $node_stmt) {
0 ignored issues
show
Bug introduced by
Accessing stmts on the interface PhpParser\Node suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
344
                if ($node_stmt instanceof PhpParser\Node\Stmt\ClassConst) {
345
                    $this->visitClassConstDeclaration($node_stmt, $storage, $fq_classlike_name);
346
                }
347
            }
348
349
            foreach ($node->stmts as $node_stmt) {
350
                if ($node_stmt instanceof PhpParser\Node\Stmt\Property) {
351
                    $this->visitPropertyDeclaration($node_stmt, $this->config, $storage, $fq_classlike_name);
352
                }
353
            }
354
        } elseif (($node instanceof PhpParser\Node\Expr\New_
355
                || $node instanceof PhpParser\Node\Expr\Instanceof_
356
                || $node instanceof PhpParser\Node\Expr\StaticPropertyFetch
357
                || $node instanceof PhpParser\Node\Expr\ClassConstFetch
358
                || $node instanceof PhpParser\Node\Expr\StaticCall)
359
            && $node->class instanceof PhpParser\Node\Name
360
        ) {
361
            $fq_classlike_name = ClassLikeChecker::getFQCLNFromNameObject($node->class, $this->aliases);
362
363
            if (!in_array(strtolower($fq_classlike_name), ['self', 'static', 'parent'], true)) {
364
                $this->codebase->scanner->queueClassLikeForScanning(
365
                    $fq_classlike_name,
366
                    $this->file_path,
367
                    false,
368
                    !($node instanceof PhpParser\Node\Expr\ClassConstFetch)
369
                        || !is_string($node->name)
370
                        || strtolower($node->name) !== 'class'
371
                );
372
                $this->file_storage->referenced_classlikes[strtolower($fq_classlike_name)] = $fq_classlike_name;
373
            }
374
        } elseif ($node instanceof PhpParser\Node\Stmt\TryCatch) {
375
            foreach ($node->catches as $catch) {
376
                foreach ($catch->types as $catch_type) {
377
                    $catch_fqcln = ClassLikeChecker::getFQCLNFromNameObject($catch_type, $this->aliases);
378
379
                    if (!in_array(strtolower($catch_fqcln), ['self', 'static', 'parent'], true)) {
380
                        $this->codebase->scanner->queueClassLikeForScanning($catch_fqcln, $this->file_path);
381
                        $this->file_storage->referenced_classlikes[strtolower($catch_fqcln)] = $catch_fqcln;
382
                    }
383
                }
384
            }
385
        } elseif ($node instanceof PhpParser\Node\FunctionLike) {
386
            $this->registerFunctionLike($node);
387
388
            if (!$this->scan_deep) {
389
                return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
390
            }
391
        } elseif ($node instanceof PhpParser\Node\Stmt\Global_) {
392
            $function_like_storage = end($this->functionlike_storages);
393
394
            if ($function_like_storage) {
395
                foreach ($node->vars as $var) {
396
                    if ($var instanceof PhpParser\Node\Expr\Variable) {
397
                        if (is_string($var->name) && $var->name !== 'argv' && $var->name !== 'argc') {
398
                            $var_id = '$' . $var->name;
399
400
                            $function_like_storage->global_variables[$var_id] = true;
401
                        }
402
                    }
403
                }
404
            }
405
        } elseif ($node instanceof PhpParser\Node\Expr\FuncCall && $node->name instanceof PhpParser\Node\Name) {
406
            $function_id = implode('\\', $node->name->parts);
407
            if (CallMap::inCallMap($function_id)) {
408
                $function_params = CallMap::getParamsFromCallMap($function_id);
409
410
                if ($function_params) {
411
                    foreach ($function_params as $function_param_group) {
412
                        foreach ($function_param_group as $function_param) {
413
                            if ($function_param->type) {
414
                                $function_param->type->queueClassLikesForScanning(
415
                                    $this->codebase,
416
                                    $this->file_storage
417
                                );
418
                            }
419
                        }
420
                    }
421
                }
422
423
                $return_type = CallMap::getReturnTypeFromCallMap($function_id);
424
425
                $return_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
426
427
                if ($function_id === 'get_class') {
428
                    $this->queue_strings_as_possible_type = true;
429
                }
430
431
                if ($function_id === 'define') {
432
                    $first_arg_value = isset($node->args[0]) ? $node->args[0]->value : null;
433
                    $second_arg_value = isset($node->args[1]) ? $node->args[1]->value : null;
434
                    if ($first_arg_value instanceof PhpParser\Node\Scalar\String_ && $second_arg_value) {
435
                        $const_type = StatementsChecker::getSimpleType(
436
                            $this->codebase,
437
                            $second_arg_value,
438
                            $this->aliases
439
                        ) ?: Type::getMixed();
440
                        $const_name = $first_arg_value->value;
441
442
                        if ($this->functionlike_storages && !$this->config->hoist_constants) {
443
                            $functionlike_storage =
444
                                $this->functionlike_storages[count($this->functionlike_storages) - 1];
445
                            $functionlike_storage->defined_constants[$const_name] = $const_type;
446
                        } else {
447
                            $this->file_storage->constants[$const_name] = $const_type;
448
                            $this->file_storage->declaring_constants[$const_name] = $this->file_path;
449
                        }
450
                    }
451
                }
452
453
                $mapping_function_ids = [];
454
455
                if (($function_id === 'array_map' && isset($node->args[0]))
456
                    || ($function_id === 'array_filter' && isset($node->args[1]))
457
                ) {
458
                    $node_arg_value = $function_id = 'array_map' ? $node->args[0]->value : $node->args[1]->value;
459
460
                    if ($node_arg_value instanceof PhpParser\Node\Scalar\String_
461
                        || $node_arg_value instanceof PhpParser\Node\Expr\Array_
462
                    ) {
463
                        $mapping_function_ids = CallChecker::getFunctionIdsFromCallableArg(
464
                            $this->file_scanner,
465
                            $node_arg_value
466
                        );
467
                    }
468
469
                    foreach ($mapping_function_ids as $potential_method_id) {
470
                        if (strpos($potential_method_id, '::') === false) {
471
                            continue;
472
                        }
473
474
                        list($callable_fqcln) = explode('::', $potential_method_id);
475
476
                        if (!in_array(strtolower($callable_fqcln), ['self', 'parent', 'static'], true)) {
477
                            $this->codebase->scanner->queueClassLikeForScanning(
478
                                $callable_fqcln,
479
                                $this->file_path
480
                            );
481
                        }
482
                    }
483
                }
484
485
                if ($function_id === 'func_get_arg'
486
                    || $function_id === 'func_get_args'
487
                    || $function_id === 'func_num_args'
488
                ) {
489
                    $function_like_storage = end($this->functionlike_storages);
490
491
                    if ($function_like_storage) {
492
                        $function_like_storage->variadic = true;
493
                    }
494
                }
495
            }
496
        } elseif ($node instanceof PhpParser\Node\Stmt\TraitUse) {
497
            if (!$this->classlike_storages) {
498
                throw new \LogicException('$this->classlike_storages should not be empty');
499
            }
500
501
            $storage = $this->classlike_storages[count($this->classlike_storages) - 1];
502
503
            $method_map = $storage->trait_alias_map ?: [];
504
505
            foreach ($node->adaptations as $adaptation) {
506
                if ($adaptation instanceof PhpParser\Node\Stmt\TraitUseAdaptation\Alias) {
507
                    if ($adaptation->method && $adaptation->newName) {
508
                        $method_map[strtolower($adaptation->method)] = strtolower($adaptation->newName);
509
                    }
510
                }
511
            }
512
513
            $storage->trait_alias_map = $method_map;
514
515
            foreach ($node->traits as $trait) {
516
                $trait_fqcln = ClassLikeChecker::getFQCLNFromNameObject($trait, $this->aliases);
517
                $this->codebase->scanner->queueClassLikeForScanning($trait_fqcln, $this->file_path, $this->scan_deep);
518
                $storage->used_traits[strtolower($trait_fqcln)] = $trait_fqcln;
519
                $this->file_storage->required_classes[strtolower($trait_fqcln)] = $trait_fqcln;
520
            }
521
        } elseif ($node instanceof PhpParser\Node\Expr\Include_) {
522
            $this->visitInclude($node);
523
        } elseif ($node instanceof PhpParser\Node\Scalar\String_ && $this->queue_strings_as_possible_type) {
524
            if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $node->value)) {
525
                $this->codebase->scanner->queueClassLikeForScanning($node->value, $this->file_path, false, false);
526
            }
527
        } elseif ($node instanceof PhpParser\Node\Expr\Assign
528
            || $node instanceof PhpParser\Node\Expr\AssignOp
529
            || $node instanceof PhpParser\Node\Expr\AssignRef
530
        ) {
531
            if ($doc_comment = $node->getDocComment()) {
532
                $var_comments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $var_comments is dead and can be removed.
Loading history...
533
534
                try {
535
                    $var_comments = CommentChecker::getTypeFromComment(
536
                        (string)$doc_comment,
537
                        $this->file_scanner,
538
                        $this->aliases,
539
                        null,
540
                        null
541
                    );
542
                } catch (DocblockParseException $e) {
543
                    // do nothing
544
                }
545
546
                foreach ($var_comments as $var_comment) {
547
                    $var_type = $var_comment->type;
548
                    $var_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
549
                }
550
            }
551
        } elseif ($node instanceof PhpParser\Node\Stmt\Const_) {
552
            foreach ($node->consts as $const) {
553
                $const_type = StatementsChecker::getSimpleType($this->codebase, $const->value, $this->aliases)
554
                    ?: Type::getMixed();
555
556
                if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
557
                    $this->codebase->addGlobalConstantType($const->name, $const_type);
558
                }
559
560
                $this->file_storage->constants[$const->name] = $const_type;
561
                $this->file_storage->declaring_constants[$const->name] = $this->file_path;
562
            }
563
        } elseif ($this->codebase->register_autoload_files && $node instanceof PhpParser\Node\Stmt\If_) {
564
            if ($node->cond instanceof PhpParser\Node\Expr\BooleanNot) {
565
                if ($node->cond->expr instanceof PhpParser\Node\Expr\FuncCall
566
                    && $node->cond->expr->name instanceof PhpParser\Node\Name
567
                ) {
568
                    if ($node->cond->expr->name->parts === ['function_exists']
569
                        && isset($node->cond->expr->args[0])
570
                        && $node->cond->expr->args[0]->value instanceof PhpParser\Node\Scalar\String_
571
                        && function_exists($node->cond->expr->args[0]->value->value)
572
                    ) {
573
                        return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
574
                    }
575
576
                    if ($node->cond->expr->name->parts === ['class_exists']
577
                        && isset($node->cond->expr->args[0])
578
                        && $node->cond->expr->args[0]->value instanceof PhpParser\Node\Scalar\String_
579
                        && class_exists($node->cond->expr->args[0]->value->value, false)
580
                    ) {
581
                        return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
582
                    }
583
                }
584
            }
585
        }
586
    }
587
588
    /**
589
     * @param  PhpParser\Node $node
590
     *
591
     * @return null
592
     */
593
    public function leaveNode(PhpParser\Node $node)
594
    {
595
        if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
596
            $this->aliases = $this->file_aliases;
597
        } elseif ($node instanceof PhpParser\Node\Stmt\ClassLike) {
598
            if (!$this->fq_classlike_names) {
599
                throw new \LogicException('$this->fq_classlike_names should not be empty');
600
            }
601
602
            $fq_classlike_name = array_pop($this->fq_classlike_names);
603
604
            if (PropertyMap::inPropertyMap($fq_classlike_name)) {
605
                $public_mapped_properties = PropertyMap::getPropertyMap()[strtolower($fq_classlike_name)];
606
607
                if (!$this->classlike_storages) {
608
                    throw new \UnexpectedValueException('$this->classlike_storages cannot be empty');
609
                }
610
611
                $storage = $this->classlike_storages[count($this->classlike_storages) - 1];
612
613
                foreach ($public_mapped_properties as $property_name => $public_mapped_property) {
614
                    $property_type = Type::parseString($public_mapped_property);
615
616
                    $property_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
617
618
                    if (!isset($storage->properties[$property_name])) {
619
                        $storage->properties[$property_name] = new PropertyStorage();
620
                    }
621
622
                    $storage->properties[$property_name]->type = $property_type;
623
                    $storage->properties[$property_name]->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
624
625
                    $property_id = $fq_classlike_name . '::$' . $property_name;
626
627
                    $storage->declaring_property_ids[$property_name] = $property_id;
628
                    $storage->appearing_property_ids[$property_name] = $property_id;
629
                }
630
            }
631
632
            $classlike_storage = array_pop($this->classlike_storages);
633
634
            if ($classlike_storage->has_visitor_issues) {
635
                $this->file_storage->has_visitor_issues = true;
636
            }
637
638
            $this->class_template_types = [];
639
640
            if ($this->after_classlike_check_plugins) {
641
                $file_manipulations = [];
642
643
                foreach ($this->after_classlike_check_plugins as $plugin_fq_class_name) {
644
                    $plugin_fq_class_name::afterVisitClassLike(
645
                        $node,
646
                        $classlike_storage,
647
                        $this->file_scanner,
648
                        $this->aliases,
649
                        $file_manipulations
650
                    );
651
                }
652
            }
653
654
            if (!$this->file_storage->has_visitor_issues) {
655
                $this->codebase->cacheClassLikeStorage($classlike_storage, $this->file_path);
656
            }
657
        } elseif ($node instanceof PhpParser\Node\Stmt\Function_
658
            || $node instanceof PhpParser\Node\Stmt\ClassMethod
659
        ) {
660
            $this->queue_strings_as_possible_type = false;
661
662
            $this->function_template_types = [];
663
        } elseif ($node instanceof PhpParser\Node\FunctionLike) {
664
            $functionlike_storage = array_pop($this->functionlike_storages);
665
666
            if ($functionlike_storage->has_visitor_issues) {
667
                $this->file_storage->has_visitor_issues = true;
668
            }
669
        }
670
671
        return null;
672
    }
673
674
    /**
675
     * @param  PhpParser\Node\FunctionLike $stmt
676
     * @param  bool $fake_method in the case of @method annotations we do something a little strange
677
     *
678
     * @return FunctionLikeStorage
679
     */
680
    private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_method = false)
681
    {
682
        $class_storage = null;
683
684
        if ($fake_method && $stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
685
            $cased_function_id = '@method ' . $stmt->name;
686
687
            $storage = new FunctionLikeStorage();
688
        } elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
689
            $cased_function_id = ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name;
690
            $function_id = strtolower($cased_function_id);
691
692
            if (isset($this->file_storage->functions[$function_id])) {
693
                if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
694
                    $this->codebase->functions->addGlobalFunction(
695
                        $function_id,
696
                        $this->file_storage->functions[$function_id]
697
                    );
698
                }
699
700
                return $this->file_storage->functions[$function_id];
701
            }
702
703
            $storage = new FunctionLikeStorage();
704
705
            if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
706
                $this->codebase->functions->addGlobalFunction($function_id, $storage);
707
            }
708
709
            $this->file_storage->functions[$function_id] = $storage;
710
            $this->file_storage->declaring_function_ids[$function_id] = strtolower($this->file_path);
711
        } elseif ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
712
            if (!$this->fq_classlike_names) {
713
                throw new \LogicException('$this->fq_classlike_names should not be null');
714
            }
715
716
            $fq_classlike_name = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
717
718
            $function_id = $fq_classlike_name . '::' . strtolower($stmt->name);
719
            $cased_function_id = $fq_classlike_name . '::' . $stmt->name;
720
721
            if (!$this->classlike_storages) {
722
                throw new \UnexpectedValueException('$class_storages cannot be empty for ' . $function_id);
723
            }
724
725
            $class_storage = $this->classlike_storages[count($this->classlike_storages) - 1];
726
727
            $storage = null;
728
729
            if (isset($class_storage->methods[strtolower($stmt->name)])) {
730
                if (!$this->codebase->register_stub_files) {
731
                    throw new \InvalidArgumentException('Cannot re-register ' . $function_id);
732
                }
733
734
                $storage = $class_storage->methods[strtolower($stmt->name)];
735
            }
736
737
            if (!$storage) {
738
                $storage = $class_storage->methods[strtolower($stmt->name)] = new MethodStorage();
739
            }
740
741
            $class_name_parts = explode('\\', $fq_classlike_name);
742
            $class_name = array_pop($class_name_parts);
743
744
            if (strtolower((string)$stmt->name) === strtolower($class_name) &&
745
                !isset($class_storage->methods['__construct']) &&
746
                strpos($fq_classlike_name, '\\') === false
747
            ) {
748
                $this->codebase->methods->setDeclaringMethodId(
749
                    $fq_classlike_name . '::__construct',
750
                    $function_id
751
                );
752
                $this->codebase->methods->setAppearingMethodId(
753
                    $fq_classlike_name . '::__construct',
754
                    $function_id
755
                );
756
            }
757
758
            $class_storage->declaring_method_ids[strtolower($stmt->name)] = $function_id;
759
            $class_storage->appearing_method_ids[strtolower($stmt->name)] = $function_id;
760
761
            if (!$stmt->isPrivate() || $stmt->name === '__construct' || $class_storage->is_trait) {
762
                $class_storage->inheritable_method_ids[strtolower($stmt->name)] = $function_id;
763
            }
764
765
            if (!isset($class_storage->overridden_method_ids[strtolower($stmt->name)])) {
766
                $class_storage->overridden_method_ids[strtolower($stmt->name)] = [];
767
            }
768
769
            $storage->is_static = (bool) $stmt->isStatic();
770
            $storage->abstract = (bool) $stmt->isAbstract();
771
772
            $storage->final = $class_storage->final || $stmt->isFinal();
773
774
            if ($stmt->isPrivate()) {
775
                $storage->visibility = ClassLikeChecker::VISIBILITY_PRIVATE;
776
            } elseif ($stmt->isProtected()) {
777
                $storage->visibility = ClassLikeChecker::VISIBILITY_PROTECTED;
778
            } else {
779
                $storage->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
780
            }
781
        } else {
782
            $function_id = $cased_function_id = $this->file_path
783
                . ':' . $stmt->getLine()
784
                . ':' . (int) $stmt->getAttribute('startFilePos') . ':-:closure';
785
786
            $storage = $this->file_storage->functions[$function_id] = new FunctionLikeStorage();
787
        }
788
789
        $this->functionlike_storages[] = $storage;
790
791
        if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
792
            $storage->cased_name = $stmt->name;
793
        } elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
794
            $storage->cased_name =
795
                ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name;
796
        }
797
798
        $storage->location = new CodeLocation($this->file_scanner, $stmt, null, true);
799
800
        $required_param_count = 0;
801
        $i = 0;
802
        $has_optional_param = false;
803
804
        $existing_params = [];
805
        $storage->params = [];
806
807
        /** @var PhpParser\Node\Param $param */
808
        foreach ($stmt->getParams() as $param) {
809
            $param_array = $this->getTranslatedFunctionParam($param);
810
811
            if (isset($existing_params['$' . $param_array->name])) {
812
                if (IssueBuffer::accepts(
813
                    new DuplicateParam(
814
                        'Duplicate param $' . $param->name . ' in docblock for ' . $cased_function_id,
815
                        new CodeLocation($this->file_scanner, $param, null, true)
816
                    )
817
                )) {
818
                    $storage->has_visitor_issues = true;
819
820
                    ++$i;
821
822
                    continue;
823
                }
824
            }
825
826
            $existing_params['$' . $param_array->name] = $i;
827
            $storage->param_types[$param_array->name] = $param_array->type;
828
            $storage->params[] = $param_array;
829
830
            if (!$param_array->is_optional) {
831
                $required_param_count = $i + 1;
832
833
                if (!$param->variadic && $has_optional_param) {
834
                    if (IssueBuffer::accepts(
835
                        new MisplacedRequiredParam(
836
                            'Required param $' . $param->name . ' should come before any optional params in ' .
837
                            $cased_function_id,
838
                            new CodeLocation($this->file_scanner, $param, null, true)
839
                        )
840
                    )) {
841
                        $storage->has_visitor_issues = true;
842
                    }
843
                }
844
            } else {
845
                $has_optional_param = true;
846
            }
847
848
            ++$i;
849
        }
850
851
        $storage->required_param_count = $required_param_count;
852
853
        if (($stmt instanceof PhpParser\Node\Stmt\Function_
854
                || $stmt instanceof PhpParser\Node\Stmt\ClassMethod)
855
            && strpos($stmt->name, 'assert') === 0
856
            && $stmt->stmts
857
        ) {
858
            $var_assertions = [];
859
860
            foreach ($stmt->stmts as $function_stmt) {
861
                if ($function_stmt instanceof PhpParser\Node\Stmt\If_) {
862
                    $final_actions = \Psalm\Checker\ScopeChecker::getFinalControlActions(
863
                        $function_stmt->stmts,
864
                        false,
865
                        false
866
                    );
867
868
                    if ($final_actions !== [\Psalm\Checker\ScopeChecker::ACTION_END]) {
869
                        continue;
870
                    }
871
872
                    $if_clauses = \Psalm\Type\Algebra::getFormula(
873
                        $function_stmt->cond,
874
                        $this->fq_classlike_names
875
                            ? $this->fq_classlike_names[count($this->fq_classlike_names) - 1]
876
                            : null,
877
                        $this->file_scanner
878
                    );
879
880
                    $negated_formula = \Psalm\Type\Algebra::negateFormula($if_clauses);
881
882
                    $rules = \Psalm\Type\Algebra::getTruthsFromFormula($negated_formula);
883
884
                    foreach ($rules as $var_id => $rule) {
885
                        foreach ($rule as $rule_part) {
886
                            if (count($rule_part) > 1) {
887
                                continue 2;
888
                            }
889
                        }
890
891
                        if (isset($existing_params[$var_id])) {
892
                            $param_offset = $existing_params[$var_id];
893
894
                            $var_assertions[] = new \Psalm\Storage\Assertion(
895
                                $param_offset,
896
                                $rule
897
                            );
898
                        } elseif (strpos($var_id, '$this->') === 0) {
899
                            $var_assertions[] = new \Psalm\Storage\Assertion(
900
                                $var_id,
901
                                $rule
902
                            );
903
                        }
904
                    }
905
                }
906
            }
907
908
            $storage->assertions = $var_assertions;
909
        }
910
911
        if (!$this->scan_deep
912
            && ($stmt instanceof PhpParser\Node\Stmt\Function_
913
                || $stmt instanceof PhpParser\Node\Stmt\ClassMethod
914
                || $stmt instanceof PhpParser\Node\Expr\Closure)
915
            && $stmt->stmts
916
        ) {
917
            // pick up func_get_args that would otherwise be missed
918
            foreach ($stmt->stmts as $function_stmt) {
919
                if ($function_stmt instanceof PhpParser\Node\Expr\Assign
920
                    && ($function_stmt->expr instanceof PhpParser\Node\Expr\FuncCall)
921
                    && ($function_stmt->expr->name instanceof PhpParser\Node\Name)
922
                ) {
923
                    $function_id = implode('\\', $function_stmt->expr->name->parts);
924
925
                    if ($function_id === 'func_get_arg'
926
                        || $function_id === 'func_get_args'
927
                        || $function_id === 'func_num_args'
928
                    ) {
929
                        $storage->variadic = true;
930
                    }
931
                }
932
            }
933
        }
934
935
        /**
936
         * @psalm-suppress MixedAssignment
937
         *
938
         * @var null|string|PhpParser\Node\Name|PhpParser\Node\NullableType
939
         */
940
        $parser_return_type = $stmt->getReturnType();
941
942
        if ($parser_return_type) {
943
            $suffix = '';
944
945
            if ($parser_return_type instanceof PhpParser\Node\NullableType) {
946
                $suffix = '|null';
947
                $parser_return_type = $parser_return_type->type;
948
            }
949
950
            if (is_string($parser_return_type)) {
951
                $return_type_string = $parser_return_type . $suffix;
952
            } else {
953
                $return_type_fq_classlike_name = ClassLikeChecker::getFQCLNFromNameObject(
954
                    $parser_return_type,
955
                    $this->aliases
956
                );
957
958
                if (!in_array(strtolower($return_type_fq_classlike_name), ['self', 'parent'], true)) {
959
                    $this->codebase->scanner->queueClassLikeForScanning(
960
                        $return_type_fq_classlike_name,
961
                        $this->file_path
962
                    );
963
                }
964
965
                $return_type_string = $return_type_fq_classlike_name . $suffix;
966
            }
967
968
            $storage->return_type = Type::parseString($return_type_string, true);
969
            $storage->return_type_location = new CodeLocation(
970
                $this->file_scanner,
971
                $stmt,
972
                null,
973
                false,
974
                CodeLocation::FUNCTION_RETURN_TYPE
975
            );
976
977
            if ($stmt->returnsByRef()) {
978
                $storage->return_type->by_ref = true;
979
            }
980
981
            $storage->signature_return_type = $storage->return_type;
982
            $storage->signature_return_type_location = $storage->return_type_location;
983
        }
984
985
        if ($stmt->returnsByRef()) {
986
            $storage->returns_by_ref = true;
987
        }
988
989
        $doc_comment = $stmt->getDocComment();
990
991
        if (!$doc_comment) {
992
            return $storage;
993
        }
994
995
        try {
996
            $docblock_info = CommentChecker::extractFunctionDocblockInfo(
997
                (string)$doc_comment,
998
                $doc_comment->getLine()
999
            );
1000
        } catch (IncorrectDocblockException $e) {
1001
            if (IssueBuffer::accepts(
1002
                new MissingDocblockType(
1003
                    $e->getMessage() . ' in docblock for ' . $cased_function_id,
1004
                    new CodeLocation($this->file_scanner, $stmt, null, true)
1005
                )
1006
            )) {
1007
                $storage->has_visitor_issues = true;
1008
            }
1009
1010
            $docblock_info = null;
1011
        } catch (DocblockParseException $e) {
1012
            if (IssueBuffer::accepts(
1013
                new InvalidDocblock(
1014
                    $e->getMessage() . ' in docblock for ' . $cased_function_id,
1015
                    new CodeLocation($this->file_scanner, $stmt, null, true)
1016
                )
1017
            )) {
1018
                $storage->has_visitor_issues = true;
1019
            }
1020
1021
            $docblock_info = null;
1022
        }
1023
1024
        if (!$docblock_info) {
1025
            return $storage;
1026
        }
1027
1028
        if ($docblock_info->deprecated) {
1029
            $storage->deprecated = true;
1030
        }
1031
1032
        if ($docblock_info->variadic) {
1033
            $storage->variadic = true;
1034
        }
1035
1036
        if ($docblock_info->ignore_nullable_return && $storage->return_type) {
1037
            $storage->return_type->ignore_nullable_issues = true;
1038
        }
1039
1040
        if ($docblock_info->ignore_falsable_return && $storage->return_type) {
1041
            $storage->return_type->ignore_falsable_issues = true;
1042
        }
1043
1044
        $storage->suppressed_issues = $docblock_info->suppress;
1045
1046
        if ($this->config->check_for_throws_docblock) {
1047
            foreach ($docblock_info->throws as $throw_class) {
1048
                $exception_fqcln = Type::getFQCLNFromString(
1049
                    $throw_class,
1050
                    $this->aliases
1051
                );
1052
1053
                $storage->throws[$exception_fqcln] = true;
1054
            }
1055
        }
1056
1057
        if (!$this->config->use_docblock_types) {
1058
            return $storage;
1059
        }
1060
1061
        $template_types = $class_storage && $class_storage->template_types ? $class_storage->template_types : null;
1062
1063
        if ($docblock_info->template_type_names) {
1064
            $storage->template_types = [];
1065
1066
            foreach ($docblock_info->template_type_names as $template_type) {
1067
                if (count($template_type) === 3) {
1068
                    $storage->template_types[$template_type[0]] = Type::parseTokens(
1069
                        Type::fixUpLocalType(
1070
                            $template_type[2],
1071
                            $this->aliases
1072
                        )
1073
                    );
1074
                } else {
1075
                    $storage->template_types[$template_type[0]] = Type::getMixed();
1076
                }
1077
            }
1078
1079
            $template_types = array_merge($template_types ?: [], $storage->template_types);
1080
1081
            $this->function_template_types = $template_types;
1082
        }
1083
1084
        if ($docblock_info->template_typeofs) {
1085
            $storage->template_typeof_params = [];
1086
1087
            foreach ($docblock_info->template_typeofs as $template_typeof) {
1088
                foreach ($storage->params as $i => $param) {
1089
                    if ($param->name === $template_typeof['param_name']) {
1090
                        $storage->template_typeof_params[$i] = $template_typeof['template_type'];
1091
                        break;
1092
                    }
1093
                }
1094
            }
1095
        }
1096
1097
        if ($docblock_info->assertions) {
1098
            $storage->assertions = [];
1099
1100
            foreach ($docblock_info->assertions as $assertion) {
1101
                foreach ($storage->params as $i => $param) {
1102
                    if ($param->name === $assertion['param_name']) {
1103
                        $storage->assertions[] = new \Psalm\Storage\Assertion(
1104
                            $i,
1105
                            [[$assertion['type']]]
1106
                        );
1107
                        continue 2;
1108
                    }
1109
                }
1110
1111
                $storage->assertions[] = new \Psalm\Storage\Assertion(
1112
                    $assertion['param_name'],
1113
                    [[$assertion['type']]]
1114
                );
1115
            }
1116
        }
1117
1118
        if ($docblock_info->if_true_assertions) {
1119
            $storage->assertions = [];
1120
1121
            foreach ($docblock_info->if_true_assertions as $assertion) {
1122
                foreach ($storage->params as $i => $param) {
1123
                    if ($param->name === $assertion['param_name']) {
1124
                        $storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
1125
                            $i,
1126
                            [[$assertion['type']]]
1127
                        );
1128
                        continue 2;
1129
                    }
1130
                }
1131
1132
                $storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
1133
                    $assertion['param_name'],
1134
                    [[$assertion['type']]]
1135
                );
1136
            }
1137
        }
1138
1139
        if ($docblock_info->if_false_assertions) {
1140
            $storage->assertions = [];
1141
1142
            foreach ($docblock_info->if_false_assertions as $assertion) {
1143
                foreach ($storage->params as $i => $param) {
1144
                    if ($param->name === $assertion['param_name']) {
1145
                        $storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
1146
                            $i,
1147
                            [[$assertion['type']]]
1148
                        );
1149
                        continue 2;
1150
                    }
1151
                }
1152
1153
                $storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
1154
                    $assertion['param_name'],
1155
                    [[$assertion['type']]]
1156
                );
1157
            }
1158
        }
1159
1160
        if ($docblock_info->return_type) {
1161
            if (!$storage->return_type || $docblock_info->return_type !== $storage->return_type->getId()) {
1162
                $storage->has_template_return_type =
1163
                    $template_types !== null &&
1164
                    count(
1165
                        array_intersect(
1166
                            Type::tokenize($docblock_info->return_type),
1167
                            array_keys($template_types)
1168
                        )
1169
                    ) > 0;
1170
1171
                $docblock_return_type = $docblock_info->return_type;
1172
1173
                if (!$storage->return_type_location) {
1174
                    $storage->return_type_location = new CodeLocation(
1175
                        $this->file_scanner,
1176
                        $stmt,
1177
                        null,
1178
                        false,
1179
                        CodeLocation::FUNCTION_PHPDOC_RETURN_TYPE,
1180
                        $docblock_info->return_type
1181
                    );
1182
                }
1183
1184
                if ($docblock_return_type) {
1185
                    try {
1186
                        $fixed_type_tokens = Type::fixUpLocalType(
1187
                            $docblock_return_type,
1188
                            $this->aliases,
1189
                            $this->function_template_types + $this->class_template_types
1190
                        );
1191
1192
                        $storage->return_type = Type::parseTokens(
1193
                            $fixed_type_tokens,
1194
                            false,
1195
                            $this->function_template_types + $this->class_template_types
1196
                        );
1197
                        $storage->return_type->setFromDocblock();
1198
1199
                        if ($storage->signature_return_type) {
1200
                            $all_typehint_types_match = true;
1201
                            $signature_return_atomic_types = $storage->signature_return_type->getTypes();
1202
1203
                            foreach ($storage->return_type->getTypes() as $key => $type) {
1204
                                if (isset($signature_return_atomic_types[$key])) {
1205
                                    $type->from_docblock = false;
1206
                                } else {
1207
                                    $all_typehint_types_match = false;
1208
                                }
1209
                            }
1210
1211
                            if ($all_typehint_types_match) {
1212
                                $storage->return_type->from_docblock = false;
1213
                            }
1214
1215
                            if ($storage->signature_return_type->isNullable()
1216
                                && !$storage->return_type->isNullable()
1217
                            ) {
1218
                                $storage->return_type->addType(new Type\Atomic\TNull());
1219
                            }
1220
                        }
1221
1222
                        $storage->return_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
1223
                    } catch (TypeParseTreeException $e) {
1224
                        if (IssueBuffer::accepts(
1225
                            new InvalidDocblock(
1226
                                $e->getMessage() . ' in docblock for ' . $cased_function_id,
1227
                                new CodeLocation($this->file_scanner, $stmt, null, true)
1228
                            )
1229
                        )) {
1230
                            $storage->has_visitor_issues = true;
1231
                        }
1232
                    }
1233
                }
1234
1235
                if ($storage->return_type && $docblock_info->ignore_nullable_return) {
1236
                    $storage->return_type->ignore_nullable_issues = true;
1237
                }
1238
1239
                if ($storage->return_type && $docblock_info->ignore_falsable_return) {
1240
                    $storage->return_type->ignore_falsable_issues = true;
1241
                }
1242
1243
                if ($stmt->returnsByRef() && $storage->return_type) {
1244
                    $storage->return_type->by_ref = true;
1245
                }
1246
1247
                if ($docblock_info->return_type_line_number) {
1248
                    $storage->return_type_location->setCommentLine($docblock_info->return_type_line_number);
1249
                }
1250
            }
1251
        }
1252
1253
        foreach ($docblock_info->globals as $global) {
1254
            try {
1255
                $storage->global_types[$global['name']] = Type::parseTokens(
1256
                    Type::fixUpLocalType(
1257
                        $global['type'],
1258
                        $this->aliases
1259
                    ),
1260
                    false
1261
                );
1262
            } catch (TypeParseTreeException $e) {
1263
                if (IssueBuffer::accepts(
1264
                    new InvalidDocblock(
1265
                        $e->getMessage() . ' in docblock for ' . $cased_function_id,
1266
                        new CodeLocation($this->file_scanner, $stmt, null, true)
1267
                    )
1268
                )) {
1269
                    $storage->has_visitor_issues = true;
1270
                }
1271
1272
                continue;
1273
            }
1274
        }
1275
1276
        if ($docblock_info->params) {
1277
            $this->improveParamsFromDocblock(
1278
                $storage,
1279
                $docblock_info->params,
1280
                $stmt
1281
            );
1282
        }
1283
1284
        return $storage;
1285
    }
1286
1287
    /**
1288
     * @param  PhpParser\Node\Param $param
1289
     *
1290
     * @return FunctionLikeParameter
1291
     */
1292
    public function getTranslatedFunctionParam(PhpParser\Node\Param $param)
1293
    {
1294
        $param_type = null;
1295
1296
        $is_nullable = $param->default !== null &&
1297
            $param->default instanceof PhpParser\Node\Expr\ConstFetch &&
1298
            $param->default->name instanceof PhpParser\Node\Name &&
1299
            strtolower($param->default->name->parts[0]) === 'null';
1300
1301
        $param_typehint = $param->type;
1302
1303
        if ($param_typehint instanceof PhpParser\Node\NullableType) {
1304
            $is_nullable = true;
1305
            $param_typehint = $param_typehint->type;
1306
        }
1307
1308
        if ($param_typehint) {
1309
            if (is_string($param_typehint)) {
1310
                $param_type_string = $param_typehint;
1311
            } elseif ($param_typehint instanceof PhpParser\Node\Name\FullyQualified) {
1312
                $param_type_string = (string)$param_typehint;
1313
                $this->codebase->scanner->queueClassLikeForScanning($param_type_string, $this->file_path);
1314
            } elseif (strtolower($param_typehint->parts[0]) === 'self') {
1315
                $param_type_string = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
1316
            } else {
1317
                $param_type_string = ClassLikeChecker::getFQCLNFromNameObject($param_typehint, $this->aliases);
1318
                if (!in_array(strtolower($param_type_string), ['self', 'static', 'parent'], true)) {
1319
                    $this->codebase->scanner->queueClassLikeForScanning($param_type_string, $this->file_path);
1320
                    $this->file_storage->referenced_classlikes[strtolower($param_type_string)] = $param_type_string;
1321
                }
1322
            }
1323
1324
            if ($param_type_string) {
1325
                if ($is_nullable) {
1326
                    $param_type_string .= '|null';
1327
                }
1328
1329
                $param_type = Type::parseString($param_type_string, true);
1330
1331
                if ($param->variadic) {
1332
                    $param_type = new Type\Union([
1333
                        new Type\Atomic\TArray([
1334
                            Type::getInt(),
1335
                            $param_type,
1336
                        ]),
1337
                    ]);
1338
                }
1339
            }
1340
        } elseif ($param->variadic) {
1341
            $param_type = new Type\Union([
1342
                new Type\Atomic\TArray([
1343
                    Type::getInt(),
1344
                    Type::getMixed(),
1345
                ]),
1346
            ]);
1347
        }
1348
1349
        $is_optional = $param->default !== null;
1350
1351
        return new FunctionLikeParameter(
1352
            $param->name,
1353
            $param->byRef,
1354
            $param_type,
1355
            new CodeLocation($this->file_scanner, $param, null, false, CodeLocation::FUNCTION_PARAM_VAR),
1356
            $param_typehint
1357
                ? new CodeLocation($this->file_scanner, $param, null, false, CodeLocation::FUNCTION_PARAM_TYPE)
1358
                : null,
1359
            $is_optional,
1360
            $is_nullable,
1361
            $param->variadic,
1362
            $param->default ? StatementsChecker::getSimpleType($this->codebase, $param->default, $this->aliases) : null
1363
        );
1364
    }
1365
1366
    /**
1367
     * @param  FunctionLikeStorage          $storage
1368
     * @param  array<int, array{type:string,name:string,line_number:int}>  $docblock_params
1369
     * @param  PhpParser\Node\FunctionLike  $function
1370
     *
1371
     * @return void
1372
     */
1373
    private function improveParamsFromDocblock(
1374
        FunctionLikeStorage $storage,
1375
        array $docblock_params,
1376
        PhpParser\Node\FunctionLike $function
1377
    ) {
1378
        $base = $this->fq_classlike_names
1379
            ? $this->fq_classlike_names[count($this->fq_classlike_names) - 1] . '::'
1380
            : '';
1381
1382
        $cased_method_id = $base . $storage->cased_name;
1383
1384
        foreach ($docblock_params as $docblock_param) {
1385
            $param_name = $docblock_param['name'];
1386
            $docblock_param_variadic = false;
1387
1388
            if (substr($param_name, 0, 3) === '...') {
1389
                $docblock_param_variadic = true;
1390
                $param_name = substr($param_name, 3);
1391
            }
1392
1393
            $param_name = substr($param_name, 1);
1394
1395
            $storage_param = null;
1396
1397
            foreach ($storage->params as $function_signature_param) {
1398
                if ($function_signature_param->name === $param_name) {
1399
                    $storage_param = $function_signature_param;
1400
                    break;
1401
                }
1402
            }
1403
1404
            if ($storage_param === null) {
1405
                continue;
1406
            }
1407
1408
            $code_location = new CodeLocation(
1409
                $this->file_scanner,
1410
                $function,
1411
                null,
1412
                true,
1413
                CodeLocation::FUNCTION_PHPDOC_PARAM_TYPE,
1414
                $docblock_param['type']
1415
            );
1416
1417
            $code_location->setCommentLine($docblock_param['line_number']);
1418
1419
            try {
1420
                $new_param_type = Type::parseTokens(
1421
                    Type::fixUpLocalType(
1422
                        $docblock_param['type'],
1423
                        $this->aliases,
1424
                        $this->function_template_types + $this->class_template_types
1425
                    ),
1426
                    false,
1427
                    $this->function_template_types + $this->class_template_types
1428
                );
1429
            } catch (TypeParseTreeException $e) {
1430
                if (IssueBuffer::accepts(
1431
                    new InvalidDocblock(
1432
                        $e->getMessage() . ' in docblock for ' . $cased_method_id,
1433
                        $code_location
1434
                    )
1435
                )) {
1436
                    $storage->has_visitor_issues = true;
1437
                }
1438
1439
                continue;
1440
            }
1441
1442
            $new_param_type->setFromDocblock();
1443
1444
            $new_param_type->queueClassLikesForScanning(
1445
                $this->codebase,
1446
                $this->file_storage,
1447
                $storage->template_types ?: []
1448
            );
1449
1450
            if ($docblock_param_variadic) {
1451
                $new_param_type = new Type\Union([
1452
                    new Type\Atomic\TArray([
1453
                        Type::getInt(),
1454
                        $new_param_type,
1455
                    ]),
1456
                ]);
1457
            }
1458
1459
            $existing_param_type_nullable = $storage_param->is_nullable;
1460
1461
            if (!$storage_param->type || $storage_param->type->isMixed() || $storage->template_types) {
1462
                if ($existing_param_type_nullable && !$new_param_type->isNullable()) {
1463
                    $new_param_type->addType(new Type\Atomic\TNull());
1464
                }
1465
1466
                if ($this->config->add_param_default_to_docblock_type
1467
                    && $storage_param->default_type
1468
                    && !$storage_param->default_type->isMixed()
1469
                    && (!$storage_param->type || !$storage_param->type->isMixed())
1470
                ) {
1471
                    $new_param_type = Type::combineUnionTypes($new_param_type, $storage_param->default_type);
1472
                }
1473
1474
                $storage_param->type = $new_param_type;
1475
                $storage_param->type_location = $code_location;
1476
                continue;
1477
            }
1478
1479
            $storage_param_atomic_types = $storage_param->type->getTypes();
1480
1481
            $all_types_match = true;
1482
            $all_typehint_types_match = true;
1483
1484
            foreach ($new_param_type->getTypes() as $key => $type) {
1485
                if (isset($storage_param_atomic_types[$key])) {
1486
                    if ($storage_param_atomic_types[$key]->getId() !== $type->getId()) {
1487
                        $all_types_match = false;
1488
                    }
1489
1490
                    $type->from_docblock = false;
1491
                } else {
1492
                    $all_types_match = false;
1493
                    $all_typehint_types_match = false;
1494
                }
1495
            }
1496
1497
            if ($all_types_match) {
1498
                continue;
1499
            }
1500
1501
            if ($all_typehint_types_match) {
1502
                $new_param_type->from_docblock = false;
1503
            }
1504
1505
            if ($existing_param_type_nullable && !$new_param_type->isNullable()) {
1506
                $new_param_type->addType(new Type\Atomic\TNull());
1507
            }
1508
1509
            $storage_param->type = $new_param_type;
1510
            $storage_param->type_location = $code_location;
1511
        }
1512
    }
1513
1514
    /**
1515
     * @param   PhpParser\Node\Stmt\Property    $stmt
1516
     * @param   Config                          $config
1517
     * @param   string                          $fq_classlike_name
1518
     *
1519
     * @return  void
1520
     */
1521
    private function visitPropertyDeclaration(
1522
        PhpParser\Node\Stmt\Property $stmt,
1523
        Config $config,
1524
        ClassLikeStorage $storage,
1525
        $fq_classlike_name
1526
    ) {
1527
        if (!$this->fq_classlike_names) {
1528
            throw new \LogicException('$this->fq_classlike_names should not be empty');
1529
        }
1530
1531
        $comment = $stmt->getDocComment();
1532
        $var_comment = null;
1533
1534
        $property_is_initialized = false;
1535
1536
        $existing_constants = $storage->protected_class_constants
1537
            + $storage->private_class_constants
1538
            + $storage->public_class_constants;
1539
1540
        if ($comment && $comment->getText() && ($config->use_docblock_types || $config->use_docblock_property_types)) {
1541
            if (preg_match('/[ \t\*]+@psalm-suppress[ \t]+PropertyNotSetInConstructor/', (string)$comment)) {
1542
                $property_is_initialized = true;
1543
            }
1544
1545
            try {
1546
                $property_type_line_number = $comment->getLine();
1547
                $var_comments = CommentChecker::getTypeFromComment(
1548
                    $comment->getText(),
1549
                    $this->file_scanner,
1550
                    $this->aliases,
1551
                    $this->function_template_types + $this->class_template_types,
1552
                    $property_type_line_number
1553
                );
1554
1555
                $var_comment = array_pop($var_comments);
1556
            } catch (IncorrectDocblockException $e) {
1557
                if (IssueBuffer::accepts(
1558
                    new MissingDocblockType(
1559
                        $e->getMessage(),
1560
                        new CodeLocation($this->file_scanner, $stmt, null, true)
1561
                    )
1562
                )) {
1563
                    $storage->has_visitor_issues = true;
1564
                }
1565
            } catch (DocblockParseException $e) {
1566
                if (IssueBuffer::accepts(
1567
                    new InvalidDocblock(
1568
                        $e->getMessage(),
1569
                        new CodeLocation($this->file_scanner, $stmt, null, true)
1570
                    )
1571
                )) {
1572
                    $storage->has_visitor_issues = true;
1573
                }
1574
            }
1575
        }
1576
1577
        $property_group_type = $var_comment ? $var_comment->type : null;
1578
1579
        if ($property_group_type) {
1580
            $property_group_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
1581
            $property_group_type->setFromDocblock();
1582
        }
1583
1584
        foreach ($stmt->props as $property) {
1585
            $property_type_location = null;
1586
            $default_type = null;
1587
1588
            if (!$property_group_type) {
1589
                if ($property->default) {
1590
                    $default_type = StatementsChecker::getSimpleType(
1591
                        $this->codebase,
1592
                        $property->default,
1593
                        $this->aliases,
1594
                        null,
1595
                        $existing_constants,
1596
                        $fq_classlike_name
1597
                    );
1598
                }
1599
1600
                $property_type = false;
1601
            } else {
1602
                if ($var_comment && $var_comment->line_number) {
1603
                    $property_type_location = new CodeLocation(
1604
                        $this->file_scanner,
1605
                        $stmt,
1606
                        null,
1607
                        false,
1608
                        CodeLocation::VAR_TYPE,
1609
                        $var_comment->original_type
1610
                    );
1611
                    $property_type_location->setCommentLine($var_comment->line_number);
1612
                }
1613
1614
                $property_type = count($stmt->props) === 1 ? $property_group_type : clone $property_group_type;
1615
            }
1616
1617
            $property_storage = $storage->properties[$property->name] = new PropertyStorage();
1618
            $property_storage->is_static = (bool)$stmt->isStatic();
1619
            $property_storage->type = $property_type;
1620
            $property_storage->location = new CodeLocation($this->file_scanner, $property);
1621
            $property_storage->type_location = $property_type_location;
1622
            $property_storage->has_default = $property->default ? true : false;
1623
            $property_storage->suggested_type = $property_group_type ? null : $default_type;
1624
            $property_storage->deprecated = $var_comment ? $var_comment->deprecated : false;
1625
1626
            if ($stmt->isPublic()) {
1627
                $property_storage->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
1628
            } elseif ($stmt->isProtected()) {
1629
                $property_storage->visibility = ClassLikeChecker::VISIBILITY_PROTECTED;
1630
            } elseif ($stmt->isPrivate()) {
1631
                $property_storage->visibility = ClassLikeChecker::VISIBILITY_PRIVATE;
1632
            }
1633
1634
            $fq_classlike_name = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
1635
1636
            $property_id = $fq_classlike_name . '::$' . $property->name;
1637
1638
            $storage->declaring_property_ids[$property->name] = $property_id;
1639
            $storage->appearing_property_ids[$property->name] = $property_id;
1640
1641
            if ($property_is_initialized) {
1642
                $storage->initialized_properties[$property->name] = true;
1643
            }
1644
1645
            if (!$stmt->isPrivate()) {
1646
                $storage->inheritable_property_ids[$property->name] = $property_id;
1647
            }
1648
        }
1649
    }
1650
1651
    /**
1652
     * @param   PhpParser\Node\Stmt\ClassConst  $stmt
1653
     * @param   string $fq_classlike_name
1654
     *
1655
     * @return  void
1656
     */
1657
    private function visitClassConstDeclaration(
1658
        PhpParser\Node\Stmt\ClassConst $stmt,
1659
        ClassLikeStorage $storage,
1660
        $fq_classlike_name
1661
    ) {
1662
        $existing_constants = $storage->protected_class_constants
1663
            + $storage->private_class_constants
1664
            + $storage->public_class_constants;
1665
1666
        foreach ($stmt->consts as $const) {
1667
            $const_type = StatementsChecker::getSimpleType(
1668
                $this->codebase,
1669
                $const->value,
1670
                $this->aliases,
1671
                null,
1672
                $existing_constants,
1673
                $fq_classlike_name
1674
            );
1675
1676
            if ($const_type) {
1677
                $existing_constants[$const->name] = $const_type;
1678
1679
                if ($stmt->isProtected()) {
1680
                    $storage->protected_class_constants[$const->name] = $const_type;
1681
                } elseif ($stmt->isPrivate()) {
1682
                    $storage->private_class_constants[$const->name] = $const_type;
1683
                } else {
1684
                    $storage->public_class_constants[$const->name] = $const_type;
1685
                }
1686
            } else {
1687
                if ($stmt->isProtected()) {
1688
                    $storage->protected_class_constant_nodes[$const->name] = $const->value;
1689
                } elseif ($stmt->isPrivate()) {
1690
                    $storage->private_class_constant_nodes[$const->name] = $const->value;
1691
                } else {
1692
                    $storage->public_class_constant_nodes[$const->name] = $const->value;
1693
                }
1694
1695
                $storage->aliases = $this->aliases;
1696
            }
1697
        }
1698
    }
1699
1700
    /**
1701
     * @param  PhpParser\Node\Expr\Include_ $stmt
1702
     *
1703
     * @return void
1704
     */
1705
    public function visitInclude(PhpParser\Node\Expr\Include_ $stmt)
1706
    {
1707
        $config = Config::getInstance();
1708
1709
        if (!$config->allow_includes) {
1710
            throw new FileIncludeException(
1711
                'File includes are not allowed per your Psalm config - check the allowFileIncludes flag.'
1712
            );
1713
        }
1714
1715
        if ($stmt->expr instanceof PhpParser\Node\Scalar\String_) {
1716
            $path_to_file = $stmt->expr->value;
1717
1718
            // attempts to resolve using get_include_path dirs
1719
            $include_path = IncludeChecker::resolveIncludePath($path_to_file, dirname($this->file_path));
1720
            $path_to_file = $include_path ? $include_path : $path_to_file;
1721
1722
            if ($path_to_file[0] !== DIRECTORY_SEPARATOR) {
1723
                $path_to_file = getcwd() . DIRECTORY_SEPARATOR . $path_to_file;
1724
            }
1725
        } else {
1726
            $path_to_file = IncludeChecker::getPathTo($stmt->expr, $this->file_path);
1727
        }
1728
1729
        if ($path_to_file) {
1730
            $reduce_pattern = '/\/[^\/]+\/\.\.\//';
1731
1732
            while (preg_match($reduce_pattern, $path_to_file)) {
1733
                $path_to_file = preg_replace($reduce_pattern, DIRECTORY_SEPARATOR, $path_to_file);
1734
            }
1735
1736
            if ($this->file_path === $path_to_file) {
1737
                return;
1738
            }
1739
1740
            if ($this->codebase->fileExists($path_to_file)) {
1741
                if ($this->scan_deep) {
1742
                    $this->codebase->scanner->addFileToDeepScan($path_to_file);
1743
                } else {
1744
                    $this->codebase->scanner->addFileToShallowScan($path_to_file);
1745
                }
1746
1747
                $this->file_storage->required_file_paths[strtolower($path_to_file)] = $path_to_file;
1748
1749
                return;
1750
            }
1751
        }
1752
1753
        return;
1754
    }
1755
1756
    /**
1757
     * @return string
1758
     */
1759
    public function getFilePath()
1760
    {
1761
        return $this->file_path;
1762
    }
1763
1764
    /**
1765
     * @return string
1766
     */
1767
    public function getFileName()
1768
    {
1769
        return $this->file_scanner->getFileName();
1770
    }
1771
1772
    /**
1773
     * @return string
1774
     */
1775
    public function getRootFilePath()
1776
    {
1777
        return $this->file_scanner->getRootFilePath();
1778
    }
1779
1780
    /**
1781
     * @return string
1782
     */
1783
    public function getRootFileName()
1784
    {
1785
        return $this->file_scanner->getRootFileName();
1786
    }
1787
1788
    /**
1789
     * @return Aliases
1790
     */
1791
    public function getAliases()
1792
    {
1793
        return $this->aliases;
1794
    }
1795
}
1796