ReflectorVisitor::enterNode()   F
last analyzed

Complexity

Conditions 123
Paths > 20000

Size

Total Lines 374

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 123
nc 63550
nop 1
dl 0
loc 374
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
namespace Psalm\Internal\PhpVisitor;
3
4
use function array_filter;
5
use function array_key_exists;
6
use function array_merge;
7
use function array_pop;
8
use function assert;
9
use function class_exists;
10
use function count;
11
use const DIRECTORY_SEPARATOR;
12
use function dirname;
13
use function end;
14
use function explode;
15
use function function_exists;
16
use function implode;
17
use function in_array;
18
use function interface_exists;
19
use function is_string;
20
use PhpParser;
21
use function preg_match;
22
use function preg_replace;
23
use Psalm\Aliases;
24
use Psalm\Codebase;
25
use Psalm\CodeLocation;
26
use Psalm\Config;
27
use Psalm\DocComment;
28
use Psalm\Exception\DocblockParseException;
29
use Psalm\Exception\FileIncludeException;
30
use Psalm\Exception\IncorrectDocblockException;
31
use Psalm\Exception\TypeParseTreeException;
32
use Psalm\FileSource;
33
use Psalm\Internal\Analyzer\ClassAnalyzer;
34
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
35
use Psalm\Internal\Analyzer\CommentAnalyzer;
36
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
37
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
38
use Psalm\Internal\Analyzer\Statements\Expression\IncludeAnalyzer;
39
use Psalm\Internal\Analyzer\Statements\Expression\SimpleTypeInferer;
40
use Psalm\Internal\Analyzer\StatementsAnalyzer;
41
use Psalm\Internal\Codebase\InternalCallMapHandler;
42
use Psalm\Internal\Codebase\PropertyMap;
43
use Psalm\Internal\Scanner\FileScanner;
44
use Psalm\Internal\Scanner\PhpStormMetaScanner;
45
use Psalm\Internal\Scanner\UnresolvedConstant;
46
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
47
use Psalm\Internal\Type\TypeAlias;
48
use Psalm\Internal\Type\TypeParser;
49
use Psalm\Internal\Type\TypeTokenizer;
50
use Psalm\Issue\DuplicateClass;
51
use Psalm\Issue\DuplicateFunction;
52
use Psalm\Issue\DuplicateMethod;
53
use Psalm\Issue\DuplicateParam;
54
use Psalm\Issue\InvalidDocblock;
55
use Psalm\Issue\MissingDocblockType;
56
use Psalm\IssueBuffer;
57
use Psalm\Storage\ClassLikeStorage;
58
use Psalm\Storage\FileStorage;
59
use Psalm\Storage\FunctionLikeParameter;
60
use Psalm\Storage\FunctionLikeStorage;
61
use Psalm\Storage\FunctionStorage;
62
use Psalm\Storage\MethodStorage;
63
use Psalm\Storage\PropertyStorage;
64
use Psalm\Type;
65
use function strpos;
66
use function strtolower;
67
use function substr;
68
use function trim;
69
use function preg_split;
70
use php_user_filter;
71
use function strlen;
72
73
/**
74
 * @internal
75
 */
76
class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParser\NodeVisitor, FileSource
77
{
78
    /** @var Aliases */
79
    private $aliases;
80
81
    /**
82
     * @var string[]
83
     */
84
    private $fq_classlike_names = [];
85
86
    /** @var FileScanner */
87
    private $file_scanner;
88
89
    /** @var Codebase */
90
    private $codebase;
91
92
    /** @var string */
93
    private $file_path;
94
95
    /** @var bool */
96
    private $scan_deep;
97
98
    /** @var Config */
99
    private $config;
100
101
    /** @var array<string, array<string, array{Type\Union}>> */
102
    private $class_template_types = [];
103
104
    /** @var array<string, array<string, array{Type\Union}>> */
105
    private $function_template_types = [];
106
107
    /** @var FunctionLikeStorage[] */
108
    private $functionlike_storages = [];
109
110
    /** @var FileStorage */
111
    private $file_storage;
112
113
    /** @var ClassLikeStorage[] */
114
    private $classlike_storages = [];
115
116
    /** @var class-string<\Psalm\Plugin\Hook\AfterClassLikeVisitInterface>[] */
117
    private $after_classlike_check_plugins;
118
119
    /** @var int */
120
    private $php_major_version;
121
122
    /** @var int */
123
    private $php_minor_version;
124
125
    /** @var PhpParser\Node\Name|null */
126
    private $namespace_name;
127
128
    /** @var PhpParser\Node\Expr|null */
129
    private $exists_cond_expr;
130
131
    /**
132
     * @var ?int
133
     */
134
    private $skip_if_descendants = null;
135
136
    /**
137
     * @var array<string, TypeAlias>
138
     */
139
    private $type_aliases = [];
140
141
    /**
142
     * @var array<string, TypeAlias\InlineTypeAlias>
143
     */
144
    private $classlike_type_aliases = [];
145
146
    public function __construct(
147
        Codebase $codebase,
148
        FileStorage $file_storage,
149
        FileScanner $file_scanner
150
    ) {
151
        $this->codebase = $codebase;
152
        $this->file_scanner = $file_scanner;
153
        $this->file_path = $file_scanner->file_path;
154
        $this->scan_deep = $file_scanner->will_analyze;
155
        $this->config = $codebase->config;
156
        $this->file_storage = $file_storage;
157
        $this->aliases = $this->file_storage->aliases = new Aliases();
158
        $this->after_classlike_check_plugins = $this->config->after_visit_classlikes;
159
        $this->php_major_version = $codebase->php_major_version;
160
        $this->php_minor_version = $codebase->php_minor_version;
161
    }
162
163
    /**
164
     * @param  PhpParser\Node $node
165
     *
166
     * @return null|int
167
     */
168
    public function enterNode(PhpParser\Node $node)
169
    {
170
        foreach ($node->getComments() as $comment) {
171
            if ($comment instanceof PhpParser\Comment\Doc) {
172
                $self_fqcln = $node instanceof PhpParser\Node\Stmt\ClassLike
173
                    && $node->name !== null
174
                    ? ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $node->name->name
175
                    : null;
176
177
                try {
178
                    $type_aliases = CommentAnalyzer::getTypeAliasesFromComment(
179
                        $comment,
180
                        $this->aliases,
181
                        $this->type_aliases,
182
                        $self_fqcln
183
                    );
184
185
                    foreach ($type_aliases as $type_alias) {
186
                        // finds issues, if there are any
187
                        TypeParser::parseTokens($type_alias->replacement_tokens);
188
                    }
189
190
                    $this->type_aliases += $type_aliases;
191
192
                    if ($type_aliases
193
                        && $node instanceof PhpParser\Node\Stmt\ClassLike
194
                    ) {
195
                        $this->classlike_type_aliases = $type_aliases;
196
                    }
197
                } catch (DocblockParseException $e) {
198
                    $this->file_storage->docblock_issues[] = new InvalidDocblock(
199
                        (string)$e->getMessage(),
200
                        new CodeLocation($this->file_scanner, $node, null, true)
201
                    );
202
                } catch (TypeParseTreeException $e) {
203
                    $this->file_storage->docblock_issues[] = new InvalidDocblock(
204
                        (string)$e->getMessage(),
205
                        new CodeLocation($this->file_scanner, $node, null, true)
206
                    );
207
                }
208
            }
209
        }
210
211
        if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
212
            $this->file_storage->aliases = $this->aliases;
213
214
            $this->namespace_name = $node->name;
215
216
            $this->aliases = new Aliases(
217
                $node->name ? implode('\\', $node->name->parts) : '',
218
                $this->aliases->uses,
219
                $this->aliases->functions,
220
                $this->aliases->constants,
221
                $this->aliases->uses_flipped,
222
                $this->aliases->functions_flipped,
223
                $this->aliases->constants_flipped
224
            );
225
226
            $this->file_storage->namespace_aliases[(int) $node->getAttribute('startFilePos')] = $this->aliases;
227
228
            if ($node->stmts) {
229
                $this->aliases->namespace_first_stmt_start = (int) $node->stmts[0]->getAttribute('startFilePos');
230
            }
231
        } elseif ($node instanceof PhpParser\Node\Stmt\Use_) {
232
            foreach ($node->uses as $use) {
233
                $use_path = implode('\\', $use->name->parts);
234
235
                $use_alias = $use->alias ? $use->alias->name : $use->name->getLast();
236
237
                switch ($use->type !== PhpParser\Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $node->type) {
238
                    case PhpParser\Node\Stmt\Use_::TYPE_FUNCTION:
239
                        $this->aliases->functions[strtolower($use_alias)] = $use_path;
240
                        $this->aliases->functions_flipped[strtolower($use_path)] = $use_alias;
241
                        break;
242
243
                    case PhpParser\Node\Stmt\Use_::TYPE_CONSTANT:
244
                        $this->aliases->constants[$use_alias] = $use_path;
245
                        $this->aliases->constants_flipped[$use_path] = $use_alias;
246
                        break;
247
248
                    case PhpParser\Node\Stmt\Use_::TYPE_NORMAL:
249
                        $this->aliases->uses[strtolower($use_alias)] = $use_path;
250
                        $this->aliases->uses_flipped[strtolower($use_path)] = $use_alias;
251
                        break;
252
                }
253
            }
254
255
            if (!$this->aliases->uses_start) {
256
                $this->aliases->uses_start = (int) $node->getAttribute('startFilePos');
257
            }
258
259
            $this->aliases->uses_end = (int) $node->getAttribute('endFilePos') + 1;
260
        } elseif ($node instanceof PhpParser\Node\Stmt\GroupUse) {
261
            $use_prefix = implode('\\', $node->prefix->parts);
262
263
            foreach ($node->uses as $use) {
264
                $use_path = $use_prefix . '\\' . implode('\\', $use->name->parts);
265
                $use_alias = $use->alias ? $use->alias->name : $use->name->getLast();
266
267
                switch ($use->type !== PhpParser\Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $node->type) {
268
                    case PhpParser\Node\Stmt\Use_::TYPE_FUNCTION:
269
                        $this->aliases->functions[strtolower($use_alias)] = $use_path;
270
                        $this->aliases->functions_flipped[strtolower($use_path)] = $use_alias;
271
                        break;
272
273
                    case PhpParser\Node\Stmt\Use_::TYPE_CONSTANT:
274
                        $this->aliases->constants[$use_alias] = $use_path;
275
                        $this->aliases->constants_flipped[$use_path] = $use_alias;
276
                        break;
277
278
                    case PhpParser\Node\Stmt\Use_::TYPE_NORMAL:
279
                        $this->aliases->uses[strtolower($use_alias)] = $use_path;
280
                        $this->aliases->uses_flipped[strtolower($use_path)] = $use_alias;
281
                        break;
282
                }
283
            }
284
285
            if (!$this->aliases->uses_start) {
286
                $this->aliases->uses_start = (int) $node->getAttribute('startFilePos');
287
            }
288
289
            $this->aliases->uses_end = (int) $node->getAttribute('endFilePos') + 1;
290
        } elseif ($node instanceof PhpParser\Node\Stmt\ClassLike) {
291
            if ($this->skip_if_descendants) {
292
                return;
293
            }
294
295
            if ($this->registerClassLike($node) === false) {
296
                return PhpParser\NodeTraverser::STOP_TRAVERSAL;
297
            }
298
        } elseif (($node instanceof PhpParser\Node\Expr\New_
299
                || $node instanceof PhpParser\Node\Expr\Instanceof_
300
                || $node instanceof PhpParser\Node\Expr\StaticPropertyFetch
301
                || $node instanceof PhpParser\Node\Expr\ClassConstFetch
302
                || $node instanceof PhpParser\Node\Expr\StaticCall)
303
            && $node->class instanceof PhpParser\Node\Name
304
        ) {
305
            $fq_classlike_name = ClassLikeAnalyzer::getFQCLNFromNameObject($node->class, $this->aliases);
306
307
            if (!in_array(strtolower($fq_classlike_name), ['self', 'static', 'parent'], true)) {
308
                $this->codebase->scanner->queueClassLikeForScanning(
309
                    $fq_classlike_name,
310
                    false,
311
                    !($node instanceof PhpParser\Node\Expr\ClassConstFetch)
312
                        || !($node->name instanceof PhpParser\Node\Identifier)
313
                        || strtolower($node->name->name) !== 'class'
314
                );
315
                $this->file_storage->referenced_classlikes[strtolower($fq_classlike_name)] = $fq_classlike_name;
316
            }
317
        } elseif ($node instanceof PhpParser\Node\Stmt\TryCatch) {
318
            foreach ($node->catches as $catch) {
319
                foreach ($catch->types as $catch_type) {
320
                    $catch_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($catch_type, $this->aliases);
321
322
                    if (!in_array(strtolower($catch_fqcln), ['self', 'static', 'parent'], true)) {
323
                        $this->codebase->scanner->queueClassLikeForScanning($catch_fqcln);
324
                        $this->file_storage->referenced_classlikes[strtolower($catch_fqcln)] = $catch_fqcln;
325
                    }
326
                }
327
            }
328
        } elseif ($node instanceof PhpParser\Node\FunctionLike) {
329
            if ($node instanceof PhpParser\Node\Stmt\Function_
330
                || $node instanceof PhpParser\Node\Stmt\ClassMethod
331
            ) {
332
                if ($this->skip_if_descendants) {
333
                    return;
334
                }
335
            }
336
337
            $this->registerFunctionLike($node);
338
339
            if ($node instanceof PhpParser\Node\Expr\Closure) {
340
                $this->codebase->scanner->queueClassLikeForScanning('Closure');
341
            }
342
343
            if (!$this->scan_deep) {
344
                return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
345
            }
346
        } elseif ($node instanceof PhpParser\Node\Stmt\Global_) {
347
            $function_like_storage = end($this->functionlike_storages);
348
349
            if ($function_like_storage) {
350
                foreach ($node->vars as $var) {
351
                    if ($var instanceof PhpParser\Node\Expr\Variable) {
352
                        if (is_string($var->name) && $var->name !== 'argv' && $var->name !== 'argc') {
353
                            $var_id = '$' . $var->name;
354
355
                            $function_like_storage->global_variables[$var_id] = true;
356
                        }
357
                    }
358
                }
359
            }
360
        } elseif ($node instanceof PhpParser\Node\Expr\FuncCall && $node->name instanceof PhpParser\Node\Name) {
361
            $function_id = implode('\\', $node->name->parts);
362
            if (InternalCallMapHandler::inCallMap($function_id)) {
363
                $this->registerClassMapFunctionCall($function_id, $node);
364
            }
365
        } elseif ($node instanceof PhpParser\Node\Stmt\TraitUse) {
366
            if ($this->skip_if_descendants) {
367
                return;
368
            }
369
370
            if (!$this->classlike_storages) {
371
                throw new \LogicException('$this->classlike_storages should not be empty');
372
            }
373
374
            $storage = $this->classlike_storages[count($this->classlike_storages) - 1];
375
376
            $method_map = $storage->trait_alias_map ?: [];
377
            $visibility_map = $storage->trait_visibility_map ?: [];
378
379
            foreach ($node->adaptations as $adaptation) {
380
                if ($adaptation instanceof PhpParser\Node\Stmt\TraitUseAdaptation\Alias) {
381
                    $old_name = strtolower($adaptation->method->name);
382
                    $new_name = $old_name;
383
384
                    if ($adaptation->newName) {
385
                        $new_name = strtolower($adaptation->newName->name);
386
387
                        if ($new_name !== $old_name) {
388
                            $method_map[$new_name] = $old_name;
389
                        }
390
                    }
391
392
                    if ($adaptation->newModifier) {
393
                        switch ($adaptation->newModifier) {
394
                            case 1:
395
                                $visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PUBLIC;
396
                                break;
397
398
                            case 2:
399
                                $visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PROTECTED;
400
                                break;
401
402
                            case 4:
403
                                $visibility_map[$new_name] = ClassLikeAnalyzer::VISIBILITY_PRIVATE;
404
                                break;
405
                        }
406
                    }
407
                }
408
            }
409
410
            $storage->trait_alias_map = $method_map;
411
            $storage->trait_visibility_map = $visibility_map;
412
413
            foreach ($node->traits as $trait) {
414
                $trait_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($trait, $this->aliases);
415
                $this->codebase->scanner->queueClassLikeForScanning($trait_fqcln, $this->scan_deep);
416
                $storage->used_traits[strtolower($trait_fqcln)] = $trait_fqcln;
417
                $this->file_storage->required_classes[strtolower($trait_fqcln)] = $trait_fqcln;
418
            }
419
420
            if ($node_comment = $node->getDocComment()) {
421
                $comments = DocComment::parsePreservingLength($node_comment);
422
423
                if (isset($comments->combined_tags['use'])) {
424
                    foreach ($comments->combined_tags['use'] as $template_line) {
425
                        $this->useTemplatedType(
426
                            $storage,
427
                            $node,
428
                            trim(preg_replace('@^[ \t]*\*@m', '', $template_line))
429
                        );
430
                    }
431
                }
432
433
                if (isset($comments->tags['template-extends'])
434
                    || isset($comments->tags['extends'])
435
                    || isset($comments->tags['template-implements'])
436
                    || isset($comments->tags['implements'])
437
                ) {
438
                    $storage->docblock_issues[] = new InvalidDocblock(
439
                        'You must use @use or @template-use to parameterize traits',
440
                        new CodeLocation($this->file_scanner, $node, null, true)
441
                    );
442
                }
443
            }
444
        } elseif ($node instanceof PhpParser\Node\Expr\Include_) {
445
            $this->visitInclude($node);
446
        } elseif ($node instanceof PhpParser\Node\Expr\Assign
447
            || $node instanceof PhpParser\Node\Expr\AssignOp
448
            || $node instanceof PhpParser\Node\Expr\AssignRef
449
            || $node instanceof PhpParser\Node\Stmt\For_
450
            || $node instanceof PhpParser\Node\Stmt\Foreach_
451
            || $node instanceof PhpParser\Node\Stmt\While_
452
            || $node instanceof PhpParser\Node\Stmt\Do_
453
        ) {
454
            if ($doc_comment = $node->getDocComment()) {
455
                $var_comments = [];
456
457
                try {
458
                    $var_comments = CommentAnalyzer::getTypeFromComment(
459
                        $doc_comment,
460
                        $this->file_scanner,
461
                        $this->aliases,
462
                        null,
463
                        $this->type_aliases
464
                    );
465
                } catch (DocblockParseException $e) {
466
                    // do nothing
467
                }
468
469
                foreach ($var_comments as $var_comment) {
470
                    if (!$var_comment->type) {
471
                        continue;
472
                    }
473
474
                    $var_type = $var_comment->type;
475
                    $var_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
476
                }
477
            }
478
479
            if ($node instanceof PhpParser\Node\Expr\Assign
480
                || $node instanceof PhpParser\Node\Expr\AssignOp
481
                || $node instanceof PhpParser\Node\Expr\AssignRef
482
            ) {
483
                if ($node->var instanceof PhpParser\Node\Expr\PropertyFetch
484
                    && $node->var->var instanceof PhpParser\Node\Expr\Variable
485
                    && $node->var->var->name === 'this'
486
                    && $node->var->name instanceof PhpParser\Node\Identifier
487
                ) {
488
                    $functionlike_storage = end($this->functionlike_storages);
489
490
                    if ($functionlike_storage instanceof MethodStorage) {
491
                        $functionlike_storage->this_property_mutations[$node->var->name->name] = true;
492
                    }
493
                }
494
            }
495
        } elseif ($node instanceof PhpParser\Node\Stmt\Const_) {
496
            foreach ($node->consts as $const) {
497
                $const_type = SimpleTypeInferer::infer(
498
                    $this->codebase,
499
                    new \Psalm\Internal\Provider\NodeDataProvider(),
500
                    $const->value,
501
                    $this->aliases
502
                ) ?: Type::getMixed();
503
504
                $fq_const_name = Type::getFQCLNFromString($const->name->name, $this->aliases);
505
506
                if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
507
                    $this->codebase->addGlobalConstantType($fq_const_name, $const_type);
508
                }
509
510
                $this->file_storage->constants[$fq_const_name] = $const_type;
511
                $this->file_storage->declaring_constants[$fq_const_name] = $this->file_path;
512
            }
513
        } elseif ($node instanceof PhpParser\Node\Stmt\If_ && !$this->skip_if_descendants) {
514
            if (!$this->fq_classlike_names && !$this->functionlike_storages) {
515
                $this->exists_cond_expr = $node->cond;
516
517
                if ($this->enterConditional($this->exists_cond_expr) === false) {
518
                    // the else node should terminate the agreement
519
                    $this->skip_if_descendants = $node->else ? $node->else->getLine() : $node->getLine();
520
                }
521
            }
522
        } elseif ($node instanceof PhpParser\Node\Stmt\Else_) {
523
            if ($this->skip_if_descendants === $node->getLine()) {
524
                $this->skip_if_descendants = null;
525
                $this->exists_cond_expr = null;
526
            } elseif (!$this->skip_if_descendants) {
527
                if ($this->exists_cond_expr && $this->enterConditional($this->exists_cond_expr) === true) {
528
                    $this->skip_if_descendants = $node->getLine();
529
                }
530
            }
531
        } elseif ($node instanceof PhpParser\Node\Expr\Yield_ || $node instanceof PhpParser\Node\Expr\YieldFrom) {
532
            $function_like_storage = end($this->functionlike_storages);
533
534
            if ($function_like_storage) {
535
                $function_like_storage->has_yield = true;
536
            }
537
        } elseif ($node instanceof PhpParser\Node\Expr\Cast\Object_) {
538
            $this->codebase->scanner->queueClassLikeForScanning('stdClass', false, false);
539
            $this->file_storage->referenced_classlikes['stdclass'] = 'stdClass';
540
        }
541
    }
542
543
    /**
544
     * @return null
545
     */
546
    public function leaveNode(PhpParser\Node $node)
547
    {
548
        if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
549
            if (!$this->file_storage->aliases) {
550
                throw new \UnexpectedValueException('File storage liases should not be null');
551
            }
552
553
            $this->aliases = $this->file_storage->aliases;
554
555
            if ($this->codebase->register_stub_files
556
                && $node->name
557
                && $node->name->parts === ['PHPSTORM_META']
558
            ) {
559
                foreach ($node->stmts as $meta_stmt) {
560
                    if ($meta_stmt instanceof PhpParser\Node\Stmt\Expression
561
                        && $meta_stmt->expr instanceof PhpParser\Node\Expr\FuncCall
562
                        && $meta_stmt->expr->name instanceof PhpParser\Node\Name
563
                        && $meta_stmt->expr->name->parts === ['override']
564
                        && count($meta_stmt->expr->args) > 1
565
                    ) {
566
                        PhpStormMetaScanner::handleOverride($meta_stmt->expr->args, $this->codebase);
567
                    }
568
                }
569
            }
570
        } elseif ($node instanceof PhpParser\Node\Stmt\ClassLike) {
571
            if ($this->skip_if_descendants) {
572
                return;
573
            }
574
575
            if (!$this->fq_classlike_names) {
576
                throw new \LogicException('$this->fq_classlike_names should not be empty');
577
            }
578
579
            $fq_classlike_name = array_pop($this->fq_classlike_names);
580
581
            if (!$this->classlike_storages) {
582
                throw new \UnexpectedValueException('$this->classlike_storages cannot be empty');
583
            }
584
585
            $classlike_storage = array_pop($this->classlike_storages);
586
587
            if (PropertyMap::inPropertyMap($fq_classlike_name)) {
588
                $mapped_properties = PropertyMap::getPropertyMap()[strtolower($fq_classlike_name)];
589
590
                foreach ($mapped_properties as $property_name => $public_mapped_property) {
591
                    $property_type = Type::parseString($public_mapped_property);
592
593
                    $property_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
594
595
                    if (!isset($classlike_storage->properties[$property_name])) {
596
                        $classlike_storage->properties[$property_name] = new PropertyStorage();
597
                    }
598
599
                    $classlike_storage->properties[$property_name]->type = $property_type;
600
601
                    $property_id = $fq_classlike_name . '::$' . $property_name;
602
603
                    $classlike_storage->declaring_property_ids[$property_name] = $fq_classlike_name;
604
                    $classlike_storage->appearing_property_ids[$property_name] = $property_id;
605
                }
606
            }
607
608
            $classlike_storage->type_aliases = \array_map(
609
                function (TypeAlias\InlineTypeAlias $t) {
610
                    $union = TypeParser::parseTokens(
611
                        $t->replacement_tokens,
612
                        null,
613
                        [],
614
                        $this->type_aliases
615
                    );
616
617
                    $union->setFromDocblock();
618
619
                    return new TypeAlias\ClassTypeAlias(
620
                        \array_values($union->getAtomicTypes())
621
                    );
622
                },
623
                $this->classlike_type_aliases
624
            );
625
626
            $this->classlike_type_aliases = [];
627
628
            if ($classlike_storage->has_visitor_issues) {
629
                $this->file_storage->has_visitor_issues = true;
630
            }
631
632
            if ($node->name) {
633
                $this->class_template_types = [];
634
            }
635
636
            if ($this->after_classlike_check_plugins) {
637
                $file_manipulations = [];
638
639
                foreach ($this->after_classlike_check_plugins as $plugin_fq_class_name) {
640
                    $plugin_fq_class_name::afterClassLikeVisit(
641
                        $node,
642
                        $classlike_storage,
643
                        $this,
644
                        $this->codebase,
645
                        $file_manipulations
646
                    );
647
                }
648
            }
649
650
            if (!$this->file_storage->has_visitor_issues) {
651
                $this->codebase->cacheClassLikeStorage($classlike_storage, $this->file_path);
652
            }
653
        } elseif ($node instanceof PhpParser\Node\FunctionLike) {
654
            if ($node instanceof PhpParser\Node\Stmt\Function_
655
                || $node instanceof PhpParser\Node\Stmt\ClassMethod
656
            ) {
657
                $this->function_template_types = [];
658
            }
659
660
            if ($this->skip_if_descendants) {
661
                return;
662
            }
663
664
            if (!$this->functionlike_storages) {
665
                if ($this->file_storage->has_visitor_issues) {
666
                    return;
667
                }
668
669
                throw new \UnexpectedValueException(
670
                    'There should be function storages for line ' . $this->file_path . ':' . $node->getLine()
671
                );
672
            }
673
674
            $functionlike_storage = array_pop($this->functionlike_storages);
675
676
            if ($functionlike_storage->has_visitor_issues) {
677
                $this->file_storage->has_visitor_issues = true;
678
            }
679
        } elseif ($node instanceof PhpParser\Node\Stmt\If_ && $node->getLine() === $this->skip_if_descendants) {
680
            $this->exists_cond_expr = null;
681
            $this->skip_if_descendants = null;
682
        } elseif ($node instanceof PhpParser\Node\Stmt\Else_ && $node->getLine() === $this->skip_if_descendants) {
683
            $this->exists_cond_expr = null;
684
            $this->skip_if_descendants = null;
685
        }
686
687
        return null;
688
    }
689
690
    private function enterConditional(PhpParser\Node\Expr $expr) : ?bool
691
    {
692
        if ($expr instanceof PhpParser\Node\Expr\BooleanNot) {
693
            $enter_negated = $this->enterConditional($expr->expr);
694
695
            return $enter_negated === null ? null : !$enter_negated;
696
        }
697
698
        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
699
            $enter_conditional_left = $this->enterConditional($expr->left);
700
            $enter_conditional_right = $this->enterConditional($expr->right);
701
702
            return $enter_conditional_left !== false && $enter_conditional_right !== false;
703
        }
704
705
        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
706
            $enter_conditional_left = $this->enterConditional($expr->left);
707
            $enter_conditional_right = $this->enterConditional($expr->right);
708
709
            return $enter_conditional_left !== false || $enter_conditional_right !== false;
710
        }
711
712
        if (!$expr instanceof PhpParser\Node\Expr\FuncCall) {
713
            return null;
714
        }
715
716
        return $this->functionEvaluatesToTrue($expr);
717
    }
718
719
    private function functionEvaluatesToTrue(PhpParser\Node\Expr\FuncCall $function) : ?bool
720
    {
721
        if (!$function->name instanceof PhpParser\Node\Name) {
722
            return null;
723
        }
724
725
        if ($function->name->parts === ['function_exists']
726
            && isset($function->args[0])
727
            && $function->args[0]->value instanceof PhpParser\Node\Scalar\String_
728
            && function_exists($function->args[0]->value->value)
729
        ) {
730
            $reflection_function = new \ReflectionFunction($function->args[0]->value->value);
731
732
            if ($reflection_function->isInternal()) {
733
                return true;
734
            }
735
736
            return false;
737
        }
738
739
        if ($function->name->parts === ['class_exists']
740
            && isset($function->args[0])
741
        ) {
742
            $string_value = null;
743
744
            if ($function->args[0]->value instanceof PhpParser\Node\Scalar\String_) {
745
                $string_value = $function->args[0]->value->value;
746
            } elseif ($function->args[0]->value instanceof PhpParser\Node\Expr\ClassConstFetch
747
                && $function->args[0]->value->class instanceof PhpParser\Node\Name
748
                && $function->args[0]->value->name instanceof PhpParser\Node\Identifier
749
                && strtolower($function->args[0]->value->name->name) === 'class'
750
            ) {
751
                $string_value = (string) $function->args[0]->value->class->getAttribute('resolvedName');
752
            }
753
754
            if ($string_value && class_exists($string_value)) {
755
                $reflection_class = new \ReflectionClass($string_value);
756
757
                if ($reflection_class->getFileName() !== $this->file_path) {
758
                    $this->codebase->scanner->queueClassLikeForScanning(
759
                        $string_value
760
                    );
761
762
                    return true;
763
                }
764
            }
765
766
            return false;
767
        }
768
769
        if ($function->name->parts === ['interface_exists']
770
            && isset($function->args[0])
771
        ) {
772
            $string_value = null;
773
774
            if ($function->args[0]->value instanceof PhpParser\Node\Scalar\String_) {
775
                $string_value = $function->args[0]->value->value;
776
            } elseif ($function->args[0]->value instanceof PhpParser\Node\Expr\ClassConstFetch
777
                && $function->args[0]->value->class instanceof PhpParser\Node\Name
778
                && $function->args[0]->value->name instanceof PhpParser\Node\Identifier
779
                && strtolower($function->args[0]->value->name->name) === 'class'
780
            ) {
781
                $string_value = (string) $function->args[0]->value->class->getAttribute('resolvedName');
782
            }
783
784
            if ($string_value && interface_exists($string_value)) {
785
                $reflection_class = new \ReflectionClass($string_value);
786
787
                if ($reflection_class->getFileName() !== $this->file_path) {
788
                    $this->codebase->scanner->queueClassLikeForScanning(
789
                        $string_value
790
                    );
791
792
                    return true;
793
                }
794
            }
795
796
            return false;
797
        }
798
799
        return null;
800
    }
801
802
    /**
803
     * @return void
804
     */
805
    private function registerClassMapFunctionCall(
806
        string $function_id,
807
        PhpParser\Node\Expr\FuncCall $node
808
    ) {
809
        $callables = InternalCallMapHandler::getCallablesFromCallMap($function_id);
810
811
        if ($callables) {
812
            foreach ($callables as $callable) {
813
                assert($callable->params !== null);
814
815
                foreach ($callable->params as $function_param) {
816
                    if ($function_param->type) {
817
                        $function_param->type->queueClassLikesForScanning(
818
                            $this->codebase,
819
                            $this->file_storage
820
                        );
821
                    }
822
                }
823
824
                if ($callable->return_type && !$callable->return_type->hasMixed()) {
825
                    $callable->return_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
826
                }
827
            }
828
        }
829
830
        if ($function_id === 'define') {
831
            $first_arg_value = isset($node->args[0]) ? $node->args[0]->value : null;
832
            $second_arg_value = isset($node->args[1]) ? $node->args[1]->value : null;
833
            if ($first_arg_value && $second_arg_value) {
834
                $type_provider = new \Psalm\Internal\Provider\NodeDataProvider();
835
                $const_name = ConstFetchAnalyzer::getConstName(
836
                    $first_arg_value,
837
                    $type_provider,
838
                    $this->codebase,
839
                    $this->aliases
840
                );
841
842
                if ($const_name !== null) {
843
                    $const_type = SimpleTypeInferer::infer(
844
                        $this->codebase,
845
                        $type_provider,
846
                        $second_arg_value,
847
                        $this->aliases
848
                    ) ?: Type::getMixed();
849
850
                    if ($this->functionlike_storages && !$this->config->hoist_constants) {
851
                        $functionlike_storage =
852
                            $this->functionlike_storages[count($this->functionlike_storages) - 1];
853
                        $functionlike_storage->defined_constants[$const_name] = $const_type;
854
                    } else {
855
                        $this->file_storage->constants[$const_name] = $const_type;
856
                        $this->file_storage->declaring_constants[$const_name] = $this->file_path;
857
                    }
858
859
                    if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
860
                        $this->codebase->addGlobalConstantType($const_name, $const_type);
861
                    }
862
                }
863
            }
864
        }
865
866
        $mapping_function_ids = [];
867
868
        if (($function_id === 'array_map' && isset($node->args[0]))
869
            || ($function_id === 'array_filter' && isset($node->args[1]))
870
        ) {
871
            $node_arg_value = $function_id === 'array_map' ? $node->args[0]->value : $node->args[1]->value;
872
873
            if ($node_arg_value instanceof PhpParser\Node\Scalar\String_
874
                || $node_arg_value instanceof PhpParser\Node\Expr\Array_
875
                || $node_arg_value instanceof PhpParser\Node\Expr\BinaryOp\Concat
876
            ) {
877
                $mapping_function_ids = CallAnalyzer::getFunctionIdsFromCallableArg(
878
                    $this->file_scanner,
879
                    $node_arg_value
880
                );
881
            }
882
883
            foreach ($mapping_function_ids as $potential_method_id) {
884
                if (strpos($potential_method_id, '::') === false) {
885
                    continue;
886
                }
887
888
                list($callable_fqcln) = explode('::', $potential_method_id);
889
890
                if (!in_array(strtolower($callable_fqcln), ['self', 'parent', 'static'], true)) {
891
                    $this->codebase->scanner->queueClassLikeForScanning(
892
                        $callable_fqcln
893
                    );
894
                }
895
            }
896
        }
897
898
        if ($function_id === 'func_get_arg'
899
            || $function_id === 'func_get_args'
900
            || $function_id === 'func_num_args'
901
        ) {
902
            $function_like_storage = end($this->functionlike_storages);
903
904
            if ($function_like_storage) {
905
                $function_like_storage->variadic = true;
906
            }
907
        }
908
909
        if ($function_id === 'is_a' || $function_id === 'is_subclass_of') {
910
            $second_arg = $node->args[1]->value ?? null;
911
912
            if ($second_arg instanceof PhpParser\Node\Scalar\String_) {
913
                $this->codebase->scanner->queueClassLikeForScanning(
914
                    $second_arg->value
915
                );
916
            }
917
        }
918
919
        if ($function_id === 'class_alias' && !$this->skip_if_descendants) {
920
            $first_arg = $node->args[0]->value ?? null;
921
            $second_arg = $node->args[1]->value ?? null;
922
923
            if ($first_arg instanceof PhpParser\Node\Scalar\String_) {
924
                $first_arg_value = $first_arg->value;
925
            } elseif ($first_arg instanceof PhpParser\Node\Expr\ClassConstFetch
926
                && $first_arg->class instanceof PhpParser\Node\Name
927
                && $first_arg->name instanceof PhpParser\Node\Identifier
928
                && strtolower($first_arg->name->name) === 'class'
929
            ) {
930
                /** @var string */
931
                $first_arg_value = $first_arg->class->getAttribute('resolvedName');
932
            } else {
933
                $first_arg_value = null;
934
            }
935
936
            if ($second_arg instanceof PhpParser\Node\Scalar\String_) {
937
                $second_arg_value = $second_arg->value;
938
            } elseif ($second_arg instanceof PhpParser\Node\Expr\ClassConstFetch
939
                && $second_arg->class instanceof PhpParser\Node\Name
940
                && $second_arg->name instanceof PhpParser\Node\Identifier
941
                && strtolower($second_arg->name->name) === 'class'
942
            ) {
943
                /** @var string */
944
                $second_arg_value = $second_arg->class->getAttribute('resolvedName');
945
            } else {
946
                $second_arg_value = null;
947
            }
948
949
            if ($first_arg_value !== null && $second_arg_value !== null) {
950
                if ($first_arg_value[0] === '\\') {
951
                    $first_arg_value = substr($first_arg_value, 1);
952
                }
953
954
                if ($second_arg_value[0] === '\\') {
955
                    $second_arg_value = substr($second_arg_value, 1);
956
                }
957
958
                $second_arg_value = strtolower($second_arg_value);
959
960
                $this->codebase->classlikes->addClassAlias(
961
                    $first_arg_value,
962
                    $second_arg_value
963
                );
964
965
                $this->file_storage->classlike_aliases[$second_arg_value] = $first_arg_value;
966
            }
967
        }
968
    }
969
970
    /**
971
     * @return false|null
972
     */
973
    private function registerClassLike(PhpParser\Node\Stmt\ClassLike $node)
974
    {
975
        $class_location = new CodeLocation($this->file_scanner, $node);
976
        $name_location = null;
977
978
        $storage = null;
979
980
        $class_name = null;
981
982
        if ($node->name === null) {
983
            if (!$node instanceof PhpParser\Node\Stmt\Class_) {
984
                throw new \LogicException('Anonymous classes are always classes');
985
            }
986
987
            $fq_classlike_name = ClassAnalyzer::getAnonymousClassName($node, $this->file_path);
988
        } else {
989
            $name_location = new CodeLocation($this->file_scanner, $node->name);
990
991
            $fq_classlike_name =
992
                ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $node->name->name;
993
994
            $fq_classlike_name_lc = strtolower($fq_classlike_name);
995
996
            $class_name = $node->name->name;
997
998
            if ($this->codebase->classlike_storage_provider->has($fq_classlike_name_lc)) {
999
                $duplicate_storage = $this->codebase->classlike_storage_provider->get($fq_classlike_name_lc);
1000
1001
                if (!$this->codebase->register_stub_files) {
1002
                    if (!$duplicate_storage->stmt_location
1003
                        || $duplicate_storage->stmt_location->file_path !== $this->file_path
1004
                        || $class_location->getHash() !== $duplicate_storage->stmt_location->getHash()
1005
                    ) {
1006
                        if (IssueBuffer::accepts(
1007
                            new DuplicateClass(
1008
                                'Class ' . $fq_classlike_name . ' has already been defined'
1009
                                    . ($duplicate_storage->location
1010
                                        ? ' in ' . $duplicate_storage->location->file_path
1011
                                        : ''),
1012
                                $name_location
1013
                            )
1014
                        )) {
1015
                        }
1016
1017
                        $this->file_storage->has_visitor_issues = true;
1018
1019
                        $duplicate_storage->has_visitor_issues = true;
1020
1021
                        return false;
1022
                    }
1023
                } elseif (!$duplicate_storage->location
1024
                    || $duplicate_storage->location->file_path !== $this->file_path
1025
                    || $class_location->getHash() !== $duplicate_storage->location->getHash()
1026
                ) {
1027
                    // we're overwriting some methods
1028
                    $storage = $duplicate_storage;
1029
                    $this->codebase->classlike_storage_provider->makeNew(strtolower($fq_classlike_name));
1030
                    $storage->populated = false;
1031
                    $storage->class_implements = []; // we do this because reflection reports
1032
                    $storage->parent_interfaces = [];
1033
                    $storage->stubbed = true;
1034
                    $storage->aliases = $this->aliases;
1035
1036
                    foreach ($storage->dependent_classlikes as $dependent_name_lc => $_) {
1037
                        $dependent_storage = $this->codebase->classlike_storage_provider->get($dependent_name_lc);
1038
                        $dependent_storage->populated = false;
1039
                        $this->codebase->classlike_storage_provider->makeNew($dependent_name_lc);
1040
                    }
1041
                }
1042
            }
1043
        }
1044
1045
        $fq_classlike_name_lc = strtolower($fq_classlike_name);
1046
1047
        $this->file_storage->classlikes_in_file[$fq_classlike_name_lc] = $fq_classlike_name;
1048
1049
        $this->fq_classlike_names[] = $fq_classlike_name;
1050
1051
        if (!$storage) {
1052
            $storage = $this->codebase->classlike_storage_provider->create($fq_classlike_name);
1053
        }
1054
1055
        if ($class_name
1056
            && isset($this->aliases->uses[strtolower($class_name)])
1057
            && $this->aliases->uses[strtolower($class_name)] !== $fq_classlike_name
1058
        ) {
1059
            IssueBuffer::add(
1060
                new \Psalm\Issue\ParseError(
1061
                    'Class name ' . $class_name . ' clashes with a use statement alias',
1062
                    $name_location ?: $class_location
1063
                )
1064
            );
1065
1066
            $storage->has_visitor_issues = true;
1067
            $this->file_storage->has_visitor_issues = true;
1068
        }
1069
1070
        $storage->stmt_location = $class_location;
1071
        $storage->location = $name_location;
1072
        if ($this->namespace_name) {
1073
            $storage->namespace_name_location = new CodeLocation($this->file_scanner, $this->namespace_name);
1074
        }
1075
        $storage->user_defined = !$this->codebase->register_stub_files;
1076
        $storage->stubbed = $this->codebase->register_stub_files;
1077
        $storage->aliases = $this->aliases;
1078
1079
        $doc_comment = $node->getDocComment();
1080
1081
        $this->classlike_storages[] = $storage;
1082
1083
        if ($node instanceof PhpParser\Node\Stmt\Class_) {
1084
            $storage->abstract = $node->isAbstract();
1085
            $storage->final = $node->isFinal();
1086
1087
            $this->codebase->classlikes->addFullyQualifiedClassName($fq_classlike_name, $this->file_path);
1088
1089
            if ($node->extends) {
1090
                $parent_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($node->extends, $this->aliases);
1091
                $parent_fqcln = $this->codebase->classlikes->getUnAliasedName($parent_fqcln);
1092
                $this->codebase->scanner->queueClassLikeForScanning(
1093
                    $parent_fqcln,
1094
                    $this->scan_deep
1095
                );
1096
                $parent_fqcln_lc = strtolower($parent_fqcln);
1097
                $storage->parent_class = $parent_fqcln;
1098
                $storage->parent_classes[$parent_fqcln_lc] = $parent_fqcln;
1099
                $this->file_storage->required_classes[strtolower($parent_fqcln)] = $parent_fqcln;
1100
            }
1101
1102
            foreach ($node->implements as $interface) {
1103
                $interface_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($interface, $this->aliases);
1104
                $this->codebase->scanner->queueClassLikeForScanning($interface_fqcln);
1105
                $storage->class_implements[strtolower($interface_fqcln)] = $interface_fqcln;
1106
                $storage->direct_class_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
1107
                $this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
1108
            }
1109
        } elseif ($node instanceof PhpParser\Node\Stmt\Interface_) {
1110
            $storage->is_interface = true;
1111
            $this->codebase->classlikes->addFullyQualifiedInterfaceName($fq_classlike_name, $this->file_path);
1112
1113
            foreach ($node->extends as $interface) {
1114
                $interface_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($interface, $this->aliases);
1115
                $interface_fqcln = $this->codebase->classlikes->getUnAliasedName($interface_fqcln);
1116
                $this->codebase->scanner->queueClassLikeForScanning($interface_fqcln);
1117
                $storage->parent_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
1118
                $storage->direct_interface_parents[strtolower($interface_fqcln)] = $interface_fqcln;
1119
                $this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
1120
            }
1121
        } elseif ($node instanceof PhpParser\Node\Stmt\Trait_) {
1122
            $storage->is_trait = true;
1123
            $this->file_storage->has_trait = true;
1124
            $this->codebase->classlikes->addFullyQualifiedTraitName($fq_classlike_name, $this->file_path);
1125
        }
1126
1127
        if ($doc_comment) {
1128
            $docblock_info = null;
1129
            try {
1130
                $docblock_info = CommentAnalyzer::extractClassLikeDocblockInfo(
1131
                    $node,
1132
                    $doc_comment,
1133
                    $this->aliases
1134
                );
1135
            } catch (DocblockParseException $e) {
1136
                $storage->docblock_issues[] = new InvalidDocblock(
1137
                    $e->getMessage() . ' in docblock for ' . implode('.', $this->fq_classlike_names),
1138
                    $name_location ?: $class_location
1139
                );
1140
            }
1141
1142
            if ($docblock_info) {
1143
                if ($docblock_info->templates) {
1144
                    $storage->template_types = [];
1145
1146
                    \usort(
1147
                        $docblock_info->templates,
1148
                        function (array $l, array $r) : int {
1149
                            return $l[4] > $r[4] ? 1 : -1;
1150
                        }
1151
                    );
1152
1153
                    foreach ($docblock_info->templates as $i => $template_map) {
1154
                        $template_name = $template_map[0];
1155
1156
                        if ($template_map[1] !== null && $template_map[2] !== null) {
1157
                            if (trim($template_map[2])) {
1158
                                try {
1159
                                    $template_type = TypeParser::parseTokens(
1160
                                        TypeTokenizer::getFullyQualifiedTokens(
1161
                                            $template_map[2],
1162
                                            $this->aliases,
1163
                                            $storage->template_types,
1164
                                            $this->type_aliases
1165
                                        ),
1166
                                        null,
1167
                                        $storage->template_types,
1168
                                        $this->type_aliases
1169
                                    );
1170
                                } catch (TypeParseTreeException $e) {
1171
                                    $storage->docblock_issues[] = new InvalidDocblock(
1172
                                        $e->getMessage() . ' in docblock for '
1173
                                            . implode('.', $this->fq_classlike_names),
1174
                                        $name_location ?: $class_location
1175
                                    );
1176
1177
                                    continue;
1178
                                }
1179
1180
                                $storage->template_types[$template_name] = [
1181
                                    $fq_classlike_name => [$template_type],
1182
                                ];
1183
                            } else {
1184
                                $storage->docblock_issues[] = new InvalidDocblock(
1185
                                    'Template missing as type',
1186
                                    $name_location ?: $class_location
1187
                                );
1188
                            }
1189
                        } else {
1190
                            /** @psalm-suppress PropertyTypeCoercion due to a Psalm bug */
1191
                            $storage->template_types[$template_name][$fq_classlike_name] = [Type::getMixed()];
1192
                        }
1193
1194
                        $storage->template_covariants[$i] = $template_map[3];
1195
                    }
1196
1197
                    $this->class_template_types = $storage->template_types;
1198
                }
1199
1200
                foreach ($docblock_info->template_extends as $extended_class_name) {
1201
                    $this->extendTemplatedType($storage, $node, $extended_class_name);
1202
                }
1203
1204
                foreach ($docblock_info->template_implements as $implemented_class_name) {
1205
                    $this->implementTemplatedType($storage, $node, $implemented_class_name);
1206
                }
1207
1208
                if ($docblock_info->yield) {
1209
                    try {
1210
                        $yield_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
1211
                            $docblock_info->yield,
1212
                            $this->aliases,
1213
                            $storage->template_types,
1214
                            $this->type_aliases
1215
                        );
1216
1217
                        $yield_type = TypeParser::parseTokens(
1218
                            $yield_type_tokens,
1219
                            null,
1220
                            $storage->template_types ?: [],
1221
                            $this->type_aliases
1222
                        );
1223
                        $yield_type->setFromDocblock();
1224
                        $yield_type->queueClassLikesForScanning(
1225
                            $this->codebase,
1226
                            $this->file_storage,
1227
                            $storage->template_types ?: []
1228
                        );
1229
1230
                        $storage->yield = $yield_type;
1231
                    } catch (TypeParseTreeException $e) {
1232
                        // do nothing
1233
                    }
1234
                }
1235
1236
                $storage->sealed_properties = $docblock_info->sealed_properties;
1237
                $storage->sealed_methods = $docblock_info->sealed_methods;
1238
1239
                if ($docblock_info->properties) {
1240
                    foreach ($docblock_info->properties as $property) {
1241
                        $pseudo_property_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
1242
                            $property['type'],
1243
                            $this->aliases,
1244
                            null,
1245
                            $this->type_aliases
1246
                        );
1247
1248
                        try {
1249
                            $pseudo_property_type = TypeParser::parseTokens($pseudo_property_type_tokens);
1250
                            $pseudo_property_type->setFromDocblock();
1251
                            $pseudo_property_type->queueClassLikesForScanning(
1252
                                $this->codebase,
1253
                                $this->file_storage,
1254
                                $storage->template_types ?: []
1255
                            );
1256
1257
                            if ($property['tag'] !== 'property-read' && $property['tag'] !== 'psalm-property-read') {
1258
                                $storage->pseudo_property_set_types[$property['name']] = $pseudo_property_type;
1259
                            }
1260
1261
                            if ($property['tag'] !== 'property-write' && $property['tag'] !== 'psalm-property-write') {
1262
                                $storage->pseudo_property_get_types[$property['name']] = $pseudo_property_type;
1263
                            }
1264
                        } catch (TypeParseTreeException $e) {
1265
                            $storage->docblock_issues[] = new InvalidDocblock(
1266
                                $e->getMessage() . ' in docblock for ' . implode('.', $this->fq_classlike_names),
1267
                                $name_location ?: $class_location
1268
                            );
1269
                        }
1270
                    }
1271
1272
                    $storage->sealed_properties = true;
1273
                }
1274
1275
                foreach ($docblock_info->methods as $method) {
1276
                    /** @var MethodStorage */
1277
                    $pseudo_method_storage = $this->registerFunctionLike($method, true);
1278
1279
                    if ($pseudo_method_storage->is_static) {
1280
                        $storage->pseudo_static_methods[strtolower($method->name->name)] = $pseudo_method_storage;
1281
                    } else {
1282
                        $storage->pseudo_methods[strtolower($method->name->name)] = $pseudo_method_storage;
1283
                    }
1284
1285
                    $storage->sealed_methods = true;
1286
                }
1287
1288
                foreach ($docblock_info->imported_types as $imported_type_data) {
1289
                    if (count($imported_type_data) > 2 && $imported_type_data[1] === 'from') {
1290
                        $type_alias_name = $as_alias_name = $imported_type_data[0];
1291
                        $declaring_classlike_name = $imported_type_data[2];
1292
1293
                        if (count($imported_type_data) > 4 && $imported_type_data[3] === 'as') {
1294
                            $as_alias_name = $imported_type_data[4];
1295
                        }
1296
1297
                        $declaring_fq_classlike_name = Type::getFQCLNFromString(
1298
                            $declaring_classlike_name,
1299
                            $this->aliases
1300
                        );
1301
1302
                        $this->codebase->scanner->queueClassLikeForScanning($declaring_fq_classlike_name);
1303
                        $this->file_storage->referenced_classlikes[strtolower($declaring_fq_classlike_name)]
1304
                            = $declaring_fq_classlike_name;
1305
1306
                        $this->type_aliases[$as_alias_name] = new TypeAlias\LinkableTypeAlias(
1307
                            $declaring_fq_classlike_name,
1308
                            $type_alias_name
1309
                        );
1310
                    }
1311
                }
1312
1313
                $storage->deprecated = $docblock_info->deprecated;
1314
                $storage->internal = $docblock_info->internal;
1315
                $storage->psalm_internal = $docblock_info->psalm_internal;
1316
                $storage->final = $storage->final || $docblock_info->final;
1317
1318
                if ($docblock_info->mixin) {
1319
                    $mixin_type = TypeParser::parseTokens(
1320
                        TypeTokenizer::getFullyQualifiedTokens(
1321
                            $docblock_info->mixin,
1322
                            $this->aliases,
1323
                            $this->class_template_types,
1324
                            $this->type_aliases,
1325
                            $fq_classlike_name
1326
                        ),
1327
                        null,
1328
                        $this->class_template_types,
1329
                        $this->type_aliases
1330
                    );
1331
1332
                    $mixin_type->queueClassLikesForScanning(
1333
                        $this->codebase,
1334
                        $this->file_storage,
1335
                        $storage->template_types ?: []
1336
                    );
1337
1338
                    $mixin_type->setFromDocblock();
1339
1340
                    if ($mixin_type->isSingle()) {
1341
                        $mixin_type = \array_values($mixin_type->getAtomicTypes())[0];
1342
1343
                        if ($mixin_type instanceof Type\Atomic\TNamedObject
1344
                            || $mixin_type instanceof Type\Atomic\TTemplateParam
1345
                        ) {
1346
                            $storage->mixin = $mixin_type;
1347
                            $storage->mixin_declaring_fqcln = $storage->name;
1348
                        }
1349
                    }
1350
                }
1351
1352
                $storage->mutation_free = $docblock_info->mutation_free;
1353
                $storage->external_mutation_free = $docblock_info->external_mutation_free;
1354
                $storage->specialize_instance = $docblock_info->taint_specialize;
1355
1356
                $storage->override_property_visibility = $docblock_info->override_property_visibility;
1357
                $storage->override_method_visibility = $docblock_info->override_method_visibility;
1358
1359
                $storage->suppressed_issues = $docblock_info->suppressed_issues;
1360
            }
1361
        }
1362
1363
        foreach ($node->stmts as $node_stmt) {
1364
            if ($node_stmt instanceof PhpParser\Node\Stmt\ClassConst) {
1365
                $this->visitClassConstDeclaration($node_stmt, $storage, $fq_classlike_name);
1366
            }
1367
        }
1368
1369
        foreach ($node->stmts as $node_stmt) {
1370
            if ($node_stmt instanceof PhpParser\Node\Stmt\Property) {
1371
                $this->visitPropertyDeclaration($node_stmt, $this->config, $storage, $fq_classlike_name);
1372
            }
1373
        }
1374
    }
1375
1376
    /**
1377
     * @return void
1378
     */
1379
    private function extendTemplatedType(
1380
        ClassLikeStorage $storage,
1381
        PhpParser\Node\Stmt\ClassLike $node,
1382
        string $extended_class_name
1383
    ) {
1384
        if (trim($extended_class_name) === '') {
1385
            $storage->docblock_issues[] = new InvalidDocblock(
1386
                'Extended class cannot be empty in docblock for ' . implode('.', $this->fq_classlike_names),
1387
                new CodeLocation($this->file_scanner, $node, null, true)
1388
            );
1389
1390
            return;
1391
        }
1392
1393
        try {
1394
            $extended_union_type = TypeParser::parseTokens(
1395
                TypeTokenizer::getFullyQualifiedTokens(
1396
                    $extended_class_name,
1397
                    $this->aliases,
1398
                    $this->class_template_types,
1399
                    $this->type_aliases
1400
                ),
1401
                null,
1402
                $this->class_template_types,
1403
                $this->type_aliases
1404
            );
1405
        } catch (TypeParseTreeException $e) {
1406
            $storage->docblock_issues[] = new InvalidDocblock(
1407
                $e->getMessage() . ' in docblock for ' . implode('.', $this->fq_classlike_names),
1408
                new CodeLocation($this->file_scanner, $node, null, true)
1409
            );
1410
1411
            return;
1412
        }
1413
1414
        if (!$extended_union_type->isSingle()) {
1415
            $storage->docblock_issues[] = new InvalidDocblock(
1416
                '@template-extends cannot be a union type',
1417
                new CodeLocation($this->file_scanner, $node, null, true)
1418
            );
1419
        }
1420
1421
        $extended_union_type->setFromDocblock();
1422
1423
        $extended_union_type->queueClassLikesForScanning(
1424
            $this->codebase,
1425
            $this->file_storage,
1426
            $storage->template_types ?: []
1427
        );
1428
1429
        foreach ($extended_union_type->getAtomicTypes() as $atomic_type) {
1430
            if (!$atomic_type instanceof Type\Atomic\TGenericObject) {
1431
                $storage->docblock_issues[] = new InvalidDocblock(
1432
                    '@template-extends has invalid class ' . $atomic_type->getId(),
1433
                    new CodeLocation($this->file_scanner, $node, null, true)
1434
                );
1435
1436
                return;
1437
            }
1438
1439
            $generic_class_lc = strtolower($atomic_type->value);
1440
1441
            if (!isset($storage->parent_classes[$generic_class_lc])
1442
                && !isset($storage->parent_interfaces[$generic_class_lc])
1443
            ) {
1444
                $storage->docblock_issues[] = new InvalidDocblock(
1445
                    '@template-extends must include the name of an extended class,'
1446
                        . ' got ' . $atomic_type->getId(),
1447
                    new CodeLocation($this->file_scanner, $node, null, true)
1448
                );
1449
            }
1450
1451
            $extended_type_parameters = [];
1452
1453
            $storage->template_type_extends_count = count($atomic_type->type_params);
1454
1455
            foreach ($atomic_type->type_params as $type_param) {
1456
                $extended_type_parameters[] = $type_param;
1457
            }
1458
1459
            $storage->template_type_extends[$atomic_type->value] = $extended_type_parameters;
1460
        }
1461
    }
1462
1463
    /**
1464
     * @return void
1465
     */
1466
    private function implementTemplatedType(
1467
        ClassLikeStorage $storage,
1468
        PhpParser\Node\Stmt\ClassLike $node,
1469
        string $implemented_class_name
1470
    ) {
1471
        if (trim($implemented_class_name) === '') {
1472
            $storage->docblock_issues[] = new InvalidDocblock(
1473
                'Extended class cannot be empty in docblock for ' . implode('.', $this->fq_classlike_names),
1474
                new CodeLocation($this->file_scanner, $node, null, true)
1475
            );
1476
1477
            return;
1478
        }
1479
1480
        try {
1481
            $implemented_union_type = TypeParser::parseTokens(
1482
                TypeTokenizer::getFullyQualifiedTokens(
1483
                    $implemented_class_name,
1484
                    $this->aliases,
1485
                    $this->class_template_types,
1486
                    $this->type_aliases
1487
                ),
1488
                null,
1489
                $this->class_template_types,
1490
                $this->type_aliases
1491
            );
1492
        } catch (TypeParseTreeException $e) {
1493
            $storage->docblock_issues[] = new InvalidDocblock(
1494
                $e->getMessage() . ' in docblock for ' . implode('.', $this->fq_classlike_names),
1495
                new CodeLocation($this->file_scanner, $node, null, true)
1496
            );
1497
1498
            return;
1499
        }
1500
1501
        if (!$implemented_union_type->isSingle()) {
1502
            $storage->docblock_issues[] = new InvalidDocblock(
1503
                '@template-implements cannot be a union type',
1504
                new CodeLocation($this->file_scanner, $node, null, true)
1505
            );
1506
1507
            return;
1508
        }
1509
1510
        $implemented_union_type->setFromDocblock();
1511
1512
        $implemented_union_type->queueClassLikesForScanning(
1513
            $this->codebase,
1514
            $this->file_storage,
1515
            $storage->template_types ?: []
1516
        );
1517
1518
        foreach ($implemented_union_type->getAtomicTypes() as $atomic_type) {
1519
            if (!$atomic_type instanceof Type\Atomic\TGenericObject) {
1520
                $storage->docblock_issues[] = new InvalidDocblock(
1521
                    '@template-implements has invalid class ' . $atomic_type->getId(),
1522
                    new CodeLocation($this->file_scanner, $node, null, true)
1523
                );
1524
1525
                return;
1526
            }
1527
1528
            $generic_class_lc = strtolower($atomic_type->value);
1529
1530
            if (!isset($storage->class_implements[$generic_class_lc])) {
1531
                $storage->docblock_issues[] = new InvalidDocblock(
1532
                    '@template-implements must include the name of an implemented class,'
1533
                        . ' got ' . $atomic_type->getId(),
1534
                    new CodeLocation($this->file_scanner, $node, null, true)
1535
                );
1536
1537
                return;
1538
            }
1539
1540
            $implemented_type_parameters = [];
1541
1542
            $storage->template_type_implements_count[$generic_class_lc] = count($atomic_type->type_params);
1543
1544
            foreach ($atomic_type->type_params as $type_param) {
1545
                $implemented_type_parameters[] = $type_param;
1546
            }
1547
1548
            $storage->template_type_extends[$atomic_type->value] = $implemented_type_parameters;
1549
        }
1550
    }
1551
1552
    /**
1553
     * @return void
1554
     */
1555
    private function useTemplatedType(
1556
        ClassLikeStorage $storage,
1557
        PhpParser\Node\Stmt\TraitUse $node,
1558
        string $used_class_name
1559
    ) {
1560
        if (trim($used_class_name) === '') {
1561
            $storage->docblock_issues[] = new InvalidDocblock(
1562
                'Extended class cannot be empty in docblock for ' . implode('.', $this->fq_classlike_names),
1563
                new CodeLocation($this->file_scanner, $node, null, true)
1564
            );
1565
1566
            return;
1567
        }
1568
1569
        try {
1570
            $used_union_type = TypeParser::parseTokens(
1571
                TypeTokenizer::getFullyQualifiedTokens(
1572
                    $used_class_name,
1573
                    $this->aliases,
1574
                    $this->class_template_types,
1575
                    $this->type_aliases
1576
                ),
1577
                null,
1578
                $this->class_template_types,
1579
                $this->type_aliases
1580
            );
1581
        } catch (TypeParseTreeException $e) {
1582
            $storage->docblock_issues[] = new InvalidDocblock(
1583
                $e->getMessage() . ' in docblock for ' . implode('.', $this->fq_classlike_names),
1584
                new CodeLocation($this->file_scanner, $node, null, true)
1585
            );
1586
1587
            return;
1588
        }
1589
1590
        if (!$used_union_type->isSingle()) {
1591
            $storage->docblock_issues[] = new InvalidDocblock(
1592
                '@template-use cannot be a union type',
1593
                new CodeLocation($this->file_scanner, $node, null, true)
1594
            );
1595
1596
            return;
1597
        }
1598
1599
        $used_union_type->setFromDocblock();
1600
1601
        $used_union_type->queueClassLikesForScanning(
1602
            $this->codebase,
1603
            $this->file_storage,
1604
            $storage->template_types ?: []
1605
        );
1606
1607
        foreach ($used_union_type->getAtomicTypes() as $atomic_type) {
1608
            if (!$atomic_type instanceof Type\Atomic\TGenericObject) {
1609
                $storage->docblock_issues[] = new InvalidDocblock(
1610
                    '@template-use has invalid class ' . $atomic_type->getId(),
1611
                    new CodeLocation($this->file_scanner, $node, null, true)
1612
                );
1613
1614
                return;
1615
            }
1616
1617
            $generic_class_lc = strtolower($atomic_type->value);
1618
1619
            if (!isset($storage->used_traits[$generic_class_lc])) {
1620
                $storage->docblock_issues[] = new InvalidDocblock(
1621
                    '@template-use must include the name of an used class,'
1622
                        . ' got ' . $atomic_type->getId(),
1623
                    new CodeLocation($this->file_scanner, $node, null, true)
1624
                );
1625
1626
                return;
1627
            }
1628
1629
            $used_type_parameters = [];
1630
1631
            $storage->template_type_uses_count[$generic_class_lc] = count($atomic_type->type_params);
1632
1633
            foreach ($atomic_type->type_params as $type_param) {
1634
                $used_type_parameters[] = $type_param;
1635
            }
1636
1637
            $storage->template_type_extends[$atomic_type->value] = $used_type_parameters;
1638
        }
1639
    }
1640
1641
    /**
1642
     * @param  PhpParser\Node\FunctionLike $stmt
1643
     * @param  bool $fake_method in the case of @method annotations we do something a little strange
1644
     *
1645
     * @return FunctionLikeStorage|false
1646
     */
1647
    private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_method = false)
1648
    {
1649
        $class_storage = null;
1650
        $fq_classlike_name = null;
1651
1652
        if ($fake_method && $stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
1653
            $cased_function_id = '@method ' . $stmt->name->name;
1654
1655
            $storage = new MethodStorage();
1656
            $storage->defining_fqcln = '';
1657
            $storage->is_static = $stmt->isStatic();
1658
            $class_storage = $this->classlike_storages[count($this->classlike_storages) - 1];
1659
            $storage->final = $class_storage->final;
1660
        } elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
1661
            $cased_function_id =
1662
                ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name->name;
1663
            $function_id = strtolower($cased_function_id);
1664
1665
            $storage = new FunctionStorage();
1666
1667
            if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
1668
                if (isset($this->file_storage->functions[$function_id])
1669
                    && ($this->codebase->register_stub_files
1670
                        || !$this->codebase->functions->hasStubbedFunction($function_id))
1671
                ) {
1672
                    $this->codebase->functions->addGlobalFunction(
1673
                        $function_id,
1674
                        $this->file_storage->functions[$function_id]
1675
                    );
1676
1677
                    $storage = $this->file_storage->functions[$function_id];
1678
                    $this->functionlike_storages[] = $storage;
1679
1680
                    return $storage;
1681
                }
1682
            } else {
1683
                if (isset($this->file_storage->functions[$function_id])) {
1684
                    $duplicate_function_storage = $this->file_storage->functions[$function_id];
1685
1686
                    if ($duplicate_function_storage->location
1687
                        && $duplicate_function_storage->location->getLineNumber() === $stmt->getLine()
1688
                    ) {
1689
                        $storage = $this->file_storage->functions[$function_id];
1690
                        $this->functionlike_storages[] = $storage;
1691
1692
                        return $storage;
1693
                    }
1694
1695
                    if (IssueBuffer::accepts(
1696
                        new DuplicateFunction(
1697
                            'Method ' . $function_id . ' has already been defined'
1698
                                . ($duplicate_function_storage->location
1699
                                    ? ' in ' . $duplicate_function_storage->location->file_path
1700
                                    : ''),
1701
                            new CodeLocation($this->file_scanner, $stmt, null, true)
1702
                        )
1703
                    )) {
1704
                        // fall through
1705
                    }
1706
1707
                    $this->file_storage->has_visitor_issues = true;
1708
1709
                    $duplicate_function_storage->has_visitor_issues = true;
1710
1711
                    $storage = $this->file_storage->functions[$function_id];
1712
                    $this->functionlike_storages[] = $storage;
1713
1714
                    return $storage;
1715
                }
1716
1717
                if (isset($this->config->getPredefinedFunctions()[$function_id])) {
1718
                    /** @psalm-suppress TypeCoercion */
1719
                    $reflection_function = new \ReflectionFunction($function_id);
1720
1721
                    if ($reflection_function->getFileName() !== $this->file_path) {
1722
                        if (IssueBuffer::accepts(
1723
                            new DuplicateFunction(
1724
                                'Method ' . $function_id . ' has already been defined as a core function',
1725
                                new CodeLocation($this->file_scanner, $stmt, null, true)
1726
                            )
1727
                        )) {
1728
                            // fall through
1729
                        }
1730
                    }
1731
                }
1732
            }
1733
1734
            if ($this->codebase->register_stub_files
1735
                || ($this->codebase->register_autoload_files
1736
                    && !$this->codebase->functions->hasStubbedFunction($function_id))
1737
            ) {
1738
                $this->codebase->functions->addGlobalFunction($function_id, $storage);
1739
            }
1740
1741
            $this->file_storage->functions[$function_id] = $storage;
1742
            $this->file_storage->declaring_function_ids[$function_id] = strtolower($this->file_path);
1743
        } elseif ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
1744
            if (!$this->fq_classlike_names) {
1745
                throw new \LogicException('$this->fq_classlike_names should not be null');
1746
            }
1747
1748
            $fq_classlike_name = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
1749
1750
            $method_name_lc = strtolower($stmt->name->name);
1751
1752
            $function_id = $fq_classlike_name . '::' . $method_name_lc;
1753
            $cased_function_id = $fq_classlike_name . '::' . $stmt->name->name;
1754
1755
            if (!$this->classlike_storages) {
1756
                throw new \UnexpectedValueException('$class_storages cannot be empty for ' . $function_id);
1757
            }
1758
1759
            $class_storage = $this->classlike_storages[count($this->classlike_storages) - 1];
1760
1761
            $storage = null;
1762
1763
            if (isset($class_storage->methods[$method_name_lc])) {
1764
                if (!$this->codebase->register_stub_files) {
1765
                    $duplicate_method_storage = $class_storage->methods[$method_name_lc];
1766
1767
                    if (IssueBuffer::accepts(
1768
                        new DuplicateMethod(
1769
                            'Method ' . $function_id . ' has already been defined'
1770
                                . ($duplicate_method_storage->location
1771
                                    ? ' in ' . $duplicate_method_storage->location->file_path
1772
                                    : ''),
1773
                            new CodeLocation($this->file_scanner, $stmt, null, true)
1774
                        )
1775
                    )) {
1776
                        // fall through
1777
                    }
1778
1779
                    $this->file_storage->has_visitor_issues = true;
1780
1781
                    $duplicate_method_storage->has_visitor_issues = true;
1782
1783
                    return false;
1784
                }
1785
1786
                $storage = $class_storage->methods[$method_name_lc];
1787
            }
1788
1789
            if (!$storage) {
1790
                $storage = $class_storage->methods[$method_name_lc] = new MethodStorage();
1791
            }
1792
1793
            $storage->defining_fqcln = $fq_classlike_name;
1794
1795
            $class_name_parts = explode('\\', $fq_classlike_name);
1796
            $class_name = array_pop($class_name_parts);
1797
1798
            if ($method_name_lc === strtolower($class_name) &&
1799
                !isset($class_storage->methods['__construct']) &&
1800
                strpos($fq_classlike_name, '\\') === false
1801
            ) {
1802
                $this->codebase->methods->setDeclaringMethodId(
1803
                    $fq_classlike_name,
1804
                    '__construct',
1805
                    $fq_classlike_name,
1806
                    $method_name_lc
1807
                );
1808
1809
                $this->codebase->methods->setAppearingMethodId(
1810
                    $fq_classlike_name,
1811
                    '__construct',
1812
                    $fq_classlike_name,
1813
                    $method_name_lc
1814
                );
1815
            }
1816
1817
            $method_id = new \Psalm\Internal\MethodIdentifier(
1818
                $fq_classlike_name,
1819
                $method_name_lc
1820
            );
1821
1822
            $class_storage->declaring_method_ids[$method_name_lc]
1823
                = $class_storage->appearing_method_ids[$method_name_lc]
1824
                = $method_id;
1825
1826
            if (!$stmt->isPrivate() || $method_name_lc === '__construct' || $class_storage->is_trait) {
1827
                $class_storage->inheritable_method_ids[$method_name_lc] = $method_id;
1828
            }
1829
1830
            if (!isset($class_storage->overridden_method_ids[$method_name_lc])) {
1831
                $class_storage->overridden_method_ids[$method_name_lc] = [];
1832
            }
1833
1834
            $storage->is_static = $stmt->isStatic();
1835
            $storage->abstract = $stmt->isAbstract();
1836
1837
            $storage->final = $class_storage->final || $stmt->isFinal();
1838
1839
            if ($stmt->isPrivate()) {
1840
                $storage->visibility = ClassLikeAnalyzer::VISIBILITY_PRIVATE;
1841
            } elseif ($stmt->isProtected()) {
1842
                $storage->visibility = ClassLikeAnalyzer::VISIBILITY_PROTECTED;
1843
            } else {
1844
                $storage->visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC;
1845
            }
1846
        } elseif ($stmt instanceof PhpParser\Node\Expr\Closure
1847
            || $stmt instanceof PhpParser\Node\Expr\ArrowFunction
1848
        ) {
1849
            $function_id = $cased_function_id = strtolower($this->file_path)
1850
                . ':' . $stmt->getLine()
1851
                . ':' . (int) $stmt->getAttribute('startFilePos') . ':-:closure';
1852
1853
            $storage = $this->file_storage->functions[$function_id] = new FunctionStorage();
1854
1855
            if ($stmt instanceof PhpParser\Node\Expr\Closure) {
1856
                foreach ($stmt->uses as $closure_use) {
1857
                    if ($closure_use->byRef && \is_string($closure_use->var->name)) {
1858
                        $storage->byref_uses[$closure_use->var->name] = true;
1859
                    }
1860
                }
1861
            }
1862
        } else {
1863
            throw new \UnexpectedValueException('Unrecognized functionlike');
1864
        }
1865
1866
        $this->functionlike_storages[] = $storage;
1867
1868
        if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
1869
            $storage->cased_name = $stmt->name->name;
1870
        } elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
1871
            $storage->cased_name =
1872
                ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name->name;
1873
        }
1874
1875
        if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod || $stmt instanceof PhpParser\Node\Stmt\Function_) {
1876
            $storage->location = new CodeLocation($this->file_scanner, $stmt->name, null, true);
1877
        } else {
1878
            $storage->location = new CodeLocation($this->file_scanner, $stmt, null, true);
1879
        }
1880
1881
        $storage->stmt_location = new CodeLocation($this->file_scanner, $stmt);
1882
1883
        $required_param_count = 0;
1884
        $i = 0;
1885
        $has_optional_param = false;
1886
1887
        $existing_params = [];
1888
        $storage->params = [];
1889
1890
        foreach ($stmt->getParams() as $param) {
1891
            if ($param->var instanceof PhpParser\Node\Expr\Error) {
1892
                $storage->docblock_issues[] = new InvalidDocblock(
1893
                    'Param' . ($i + 1) . ' of ' . $cased_function_id . ' has invalid syntax',
1894
                    new CodeLocation($this->file_scanner, $param, null, true)
1895
                );
1896
1897
                ++$i;
1898
1899
                continue;
1900
            }
1901
1902
            $param_array = $this->getTranslatedFunctionParam($param, $stmt, $fake_method, $fq_classlike_name);
1903
1904
            if (isset($existing_params['$' . $param_array->name])) {
1905
                $storage->docblock_issues[] = new DuplicateParam(
1906
                    'Duplicate param $' . $param_array->name . ' in docblock for ' . $cased_function_id,
1907
                    new CodeLocation($this->file_scanner, $param, null, true)
1908
                );
1909
1910
                ++$i;
1911
1912
                continue;
1913
            }
1914
1915
            $existing_params['$' . $param_array->name] = $i;
1916
            $storage->param_lookup[$param_array->name] = !!$param->type;
1917
            $storage->params[] = $param_array;
1918
1919
            if (!$param_array->is_optional && !$param_array->is_variadic) {
1920
                $required_param_count = $i + 1;
1921
1922
                if (!$param->variadic
1923
                    && $has_optional_param
1924
                ) {
1925
                    foreach ($storage->params as $param) {
1926
                        $param->is_optional = false;
1927
                    }
1928
                }
1929
            } else {
1930
                $has_optional_param = true;
1931
            }
1932
1933
            ++$i;
1934
        }
1935
1936
        $storage->required_param_count = $required_param_count;
1937
1938
        if (($stmt instanceof PhpParser\Node\Stmt\Function_
1939
                || $stmt instanceof PhpParser\Node\Stmt\ClassMethod)
1940
            && $stmt->stmts
1941
        ) {
1942
            if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
1943
                && $storage instanceof MethodStorage
1944
                && $class_storage
1945
                && !$class_storage->mutation_free
1946
                && count($stmt->stmts) === 1
1947
                && !count($stmt->params)
1948
                && $stmt->stmts[0] instanceof PhpParser\Node\Stmt\Return_
1949
                && $stmt->stmts[0]->expr instanceof PhpParser\Node\Expr\PropertyFetch
1950
                && $stmt->stmts[0]->expr->var instanceof PhpParser\Node\Expr\Variable
1951
                && $stmt->stmts[0]->expr->var->name === 'this'
1952
                && $stmt->stmts[0]->expr->name instanceof PhpParser\Node\Identifier
1953
            ) {
1954
                $property_name = $stmt->stmts[0]->expr->name->name;
1955
1956
                if (isset($class_storage->properties[$property_name])
1957
                    && $class_storage->properties[$property_name]->type
1958
                ) {
1959
                    $storage->mutation_free = true;
1960
                    $storage->external_mutation_free = true;
1961
                    $storage->mutation_free_inferred = !$stmt->isFinal() && !$class_storage->final;
0 ignored issues
show
Bug introduced by
The method isFinal does only exist in PhpParser\Node\Stmt\ClassMethod, but not in PhpParser\Node\Stmt\Function_.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1962
1963
                    $class_storage->properties[$property_name]->getter_method = strtolower($stmt->name->name);
1964
                }
1965
            } elseif (strpos($stmt->name->name, 'assert') === 0) {
1966
                $var_assertions = [];
1967
1968
                foreach ($stmt->stmts as $function_stmt) {
1969
                    if ($function_stmt instanceof PhpParser\Node\Stmt\If_) {
1970
                        $final_actions = \Psalm\Internal\Analyzer\ScopeAnalyzer::getFinalControlActions(
1971
                            $function_stmt->stmts,
1972
                            null,
1973
                            $this->config->exit_functions,
1974
                            [],
1975
                            false
1976
                        );
1977
1978
                        if ($final_actions !== [\Psalm\Internal\Analyzer\ScopeAnalyzer::ACTION_END]) {
1979
                            $var_assertions = [];
1980
                            break;
1981
                        }
1982
1983
                        $if_clauses = \Psalm\Type\Algebra::getFormula(
1984
                            \spl_object_id($function_stmt->cond),
1985
                            $function_stmt->cond,
1986
                            $this->fq_classlike_names
1987
                                ? $this->fq_classlike_names[count($this->fq_classlike_names) - 1]
1988
                                : null,
1989
                            $this->file_scanner,
1990
                            null
1991
                        );
1992
1993
                        $negated_formula = \Psalm\Type\Algebra::negateFormula($if_clauses);
1994
1995
                        $rules = \Psalm\Type\Algebra::getTruthsFromFormula($negated_formula);
1996
1997
                        if (!$rules) {
1998
                            $var_assertions = [];
1999
                            break;
2000
                        }
2001
2002
                        foreach ($rules as $var_id => $rule) {
2003
                            foreach ($rule as $rule_part) {
2004
                                if (count($rule_part) > 1) {
2005
                                    continue 2;
2006
                                }
2007
                            }
2008
2009
                            if (isset($existing_params[$var_id])) {
2010
                                $param_offset = $existing_params[$var_id];
2011
2012
                                $var_assertions[] = new \Psalm\Storage\Assertion(
2013
                                    $param_offset,
2014
                                    $rule
2015
                                );
2016
                            } elseif (strpos($var_id, '$this->') === 0) {
2017
                                $var_assertions[] = new \Psalm\Storage\Assertion(
2018
                                    $var_id,
2019
                                    $rule
2020
                                );
2021
                            }
2022
                        }
2023
                    } else {
2024
                        $var_assertions = [];
2025
                        break;
2026
                    }
2027
                }
2028
2029
                $storage->assertions = $var_assertions;
2030
            }
2031
        }
2032
2033
        if (!$this->scan_deep
2034
            && ($stmt instanceof PhpParser\Node\Stmt\Function_
2035
                || $stmt instanceof PhpParser\Node\Stmt\ClassMethod
2036
                || $stmt instanceof PhpParser\Node\Expr\Closure)
2037
            && $stmt->stmts
2038
        ) {
2039
            // pick up func_get_args that would otherwise be missed
2040
            foreach ($stmt->stmts as $function_stmt) {
2041
                if ($function_stmt instanceof PhpParser\Node\Stmt\Expression
2042
                    && $function_stmt->expr instanceof PhpParser\Node\Expr\Assign
2043
                    && $function_stmt->expr->expr instanceof PhpParser\Node\Expr\FuncCall
2044
                    && $function_stmt->expr->expr->name instanceof PhpParser\Node\Name
2045
                ) {
2046
                    $function_id = implode('\\', $function_stmt->expr->expr->name->parts);
2047
2048
                    if ($function_id === 'func_get_arg'
2049
                        || $function_id === 'func_get_args'
2050
                        || $function_id === 'func_num_args'
2051
                    ) {
2052
                        $storage->variadic = true;
2053
                    }
2054
                } elseif ($function_stmt instanceof PhpParser\Node\Stmt\If_
2055
                    && $function_stmt->cond instanceof PhpParser\Node\Expr\BinaryOp
2056
                    && $function_stmt->cond->left instanceof PhpParser\Node\Expr\BinaryOp\Equal
2057
                    && $function_stmt->cond->left->left instanceof PhpParser\Node\Expr\FuncCall
2058
                    && $function_stmt->cond->left->left->name instanceof PhpParser\Node\Name
2059
                ) {
2060
                    $function_id = implode('\\', $function_stmt->cond->left->left->name->parts);
2061
2062
                    if ($function_id === 'func_get_arg'
2063
                        || $function_id === 'func_get_args'
2064
                        || $function_id === 'func_num_args'
2065
                    ) {
2066
                        $storage->variadic = true;
2067
                    }
2068
                }
2069
            }
2070
        }
2071
2072
        $parser_return_type = $stmt->getReturnType();
2073
2074
        if ($parser_return_type) {
2075
            $suffix = '';
2076
2077
            $original_type = $parser_return_type;
2078
2079
            if ($parser_return_type instanceof PhpParser\Node\NullableType) {
2080
                $suffix = '|null';
2081
                $parser_return_type = $parser_return_type->type;
2082
            }
2083
2084
            if ($parser_return_type instanceof PhpParser\Node\Identifier) {
2085
                $return_type_string = $parser_return_type->name . $suffix;
2086
            } elseif ($parser_return_type instanceof PhpParser\Node\UnionType) {
2087
                // for now unsupported
2088
                $return_type_string = 'mixed';
2089
            } else {
2090
                $return_type_fq_classlike_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
2091
                    $parser_return_type,
2092
                    $this->aliases
2093
                );
2094
2095
                if ($class_storage && !$class_storage->is_trait && $return_type_fq_classlike_name === 'self') {
2096
                    $return_type_fq_classlike_name = $class_storage->name;
2097
                }
2098
2099
                $return_type_string = $return_type_fq_classlike_name . $suffix;
2100
            }
2101
2102
            $storage->return_type = Type::parseString(
2103
                $return_type_string,
2104
                [$this->php_major_version, $this->php_minor_version]
2105
            );
2106
            $storage->return_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
2107
2108
            $storage->return_type_location = new CodeLocation(
2109
                $this->file_scanner,
2110
                $original_type
2111
            );
2112
2113
            if ($stmt->returnsByRef()) {
2114
                $storage->return_type->by_ref = true;
2115
            }
2116
2117
            $storage->signature_return_type = $storage->return_type;
2118
            $storage->signature_return_type_location = $storage->return_type_location;
2119
        }
2120
2121
        if ($stmt->returnsByRef()) {
2122
            $storage->returns_by_ref = true;
2123
        }
2124
2125
        $doc_comment = $stmt->getDocComment();
2126
2127
2128
        if ($class_storage && ! $class_storage->is_trait) {
2129
            $storage->internal = $class_storage->internal;
2130
            $storage->psalm_internal = $class_storage->psalm_internal;
2131
        }
2132
2133
        if (!$doc_comment) {
2134
            if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
2135
                && $stmt->name->name === '__construct'
2136
                && $class_storage
2137
                && $storage instanceof MethodStorage
2138
                && $storage->params
2139
                && $this->config->infer_property_types_from_constructor
2140
            ) {
2141
                $this->inferPropertyTypeFromConstructor($stmt, $storage, $class_storage);
2142
            }
2143
2144
            return $storage;
2145
        }
2146
2147
        try {
2148
            $docblock_info = CommentAnalyzer::extractFunctionDocblockInfo($doc_comment);
2149
        } catch (IncorrectDocblockException $e) {
2150
            $storage->docblock_issues[] = new MissingDocblockType(
2151
                $e->getMessage() . ' in docblock for ' . $cased_function_id,
2152
                new CodeLocation($this->file_scanner, $stmt, null, true)
2153
            );
2154
2155
            $docblock_info = null;
2156
        } catch (DocblockParseException $e) {
2157
            $storage->docblock_issues[] = new InvalidDocblock(
2158
                $e->getMessage() . ' in docblock for ' . $cased_function_id,
2159
                new CodeLocation($this->file_scanner, $stmt, null, true)
2160
            );
2161
2162
            $docblock_info = null;
2163
        }
2164
2165
        if (!$docblock_info) {
2166
            return $storage;
2167
        }
2168
2169
        if ($docblock_info->mutation_free) {
2170
            $storage->mutation_free = true;
2171
2172
            if ($storage instanceof MethodStorage) {
2173
                $storage->external_mutation_free = true;
2174
                $storage->mutation_free_inferred = false;
2175
            }
2176
        }
2177
2178
        if ($storage instanceof MethodStorage && $docblock_info->external_mutation_free) {
2179
            $storage->external_mutation_free = true;
2180
        }
2181
2182
        if ($docblock_info->deprecated) {
2183
            $storage->deprecated = true;
2184
        }
2185
2186
        if ($docblock_info->internal) {
2187
            $storage->internal = true;
2188
        }
2189
2190
        if (null === $class_storage ||
2191
            null === $class_storage->psalm_internal ||
2192
            (null !== $docblock_info->psalm_internal &&
2193
                strlen($docblock_info->psalm_internal) > strlen($class_storage->psalm_internal)
2194
            )
2195
        ) {
2196
            $storage->psalm_internal = $docblock_info->psalm_internal;
2197
        }
2198
2199
        if ($docblock_info->variadic) {
2200
            $storage->variadic = true;
2201
        }
2202
2203
        if ($docblock_info->pure) {
2204
            $storage->pure = true;
2205
            $storage->specialize_call = true;
2206
            $storage->mutation_free = true;
2207
            if ($storage instanceof MethodStorage) {
2208
                $storage->external_mutation_free = true;
2209
            }
2210
        }
2211
2212
        if ($docblock_info->specialize_call) {
2213
            $storage->specialize_call = true;
2214
        }
2215
2216
        if ($docblock_info->ignore_nullable_return && $storage->return_type) {
2217
            $storage->return_type->ignore_nullable_issues = true;
2218
        }
2219
2220
        if ($docblock_info->ignore_falsable_return && $storage->return_type) {
2221
            $storage->return_type->ignore_falsable_issues = true;
2222
        }
2223
2224
        $storage->suppressed_issues = $docblock_info->suppressed_issues;
2225
2226
        foreach ($docblock_info->throws as [$throw, $offset, $line]) {
2227
            $throw_location = new CodeLocation\DocblockTypeLocation(
2228
                $this->file_scanner,
2229
                $offset,
0 ignored issues
show
Bug introduced by
The variable $offset does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
2230
                $offset + \strlen($throw),
0 ignored issues
show
Bug introduced by
The variable $throw does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
2231
                $line
0 ignored issues
show
Bug introduced by
The variable $line does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
2232
            );
2233
2234
            $class_names = \array_filter(\array_map('trim', explode('|', $throw)));
2235
2236
            foreach ($class_names as $throw_class) {
2237
                if ($throw_class !== 'self' && $throw_class !== 'static' && $throw_class !== 'parent') {
2238
                    $exception_fqcln = Type::getFQCLNFromString(
2239
                        $throw_class,
2240
                        $this->aliases
2241
                    );
2242
                } else {
2243
                    $exception_fqcln = $throw_class;
2244
                }
2245
2246
                $this->codebase->scanner->queueClassLikeForScanning($exception_fqcln);
2247
                $this->file_storage->referenced_classlikes[strtolower($exception_fqcln)] = $exception_fqcln;
2248
                $storage->throws[$exception_fqcln] = true;
2249
                $storage->throw_locations[$exception_fqcln] = $throw_location;
2250
            }
2251
        }
2252
2253
        if (!$this->config->use_docblock_types) {
2254
            return $storage;
2255
        }
2256
2257
        if ($storage instanceof MethodStorage && $docblock_info->inheritdoc) {
2258
            $storage->inheritdoc = true;
2259
        }
2260
2261
        $template_types = $class_storage && $class_storage->template_types ? $class_storage->template_types : null;
2262
2263
        if ($docblock_info->templates) {
2264
            $storage->template_types = [];
2265
2266
            foreach ($docblock_info->templates as $i => $template_map) {
2267
                $template_name = $template_map[0];
2268
2269
                if ($template_map[1] !== null && $template_map[2] !== null) {
2270
                    if (trim($template_map[2])) {
2271
                        try {
2272
                            $template_type = TypeParser::parseTokens(
2273
                                TypeTokenizer::getFullyQualifiedTokens(
2274
                                    $template_map[2],
2275
                                    $this->aliases,
2276
                                    $storage->template_types + ($template_types ?: []),
2277
                                    $this->type_aliases
2278
                                ),
2279
                                null,
2280
                                $storage->template_types + ($template_types ?: []),
2281
                                $this->type_aliases
2282
                            );
2283
                        } catch (TypeParseTreeException $e) {
2284
                            $storage->docblock_issues[] = new InvalidDocblock(
2285
                                'Template ' . $template_name . ' has invalid as type - ' . $e->getMessage(),
2286
                                new CodeLocation($this->file_scanner, $stmt, null, true)
2287
                            );
2288
2289
                            $template_type = Type::getMixed();
2290
                        }
2291
                    } else {
2292
                        $storage->docblock_issues[] = new InvalidDocblock(
2293
                            'Template ' . $template_name . ' missing as type',
2294
                            new CodeLocation($this->file_scanner, $stmt, null, true)
2295
                        );
2296
2297
                        $template_type = Type::getMixed();
2298
                    }
2299
                } else {
2300
                    $template_type = Type::getMixed();
2301
                }
2302
2303
                if (isset($template_types[$template_name])) {
2304
                    $storage->docblock_issues[] = new InvalidDocblock(
2305
                        'Duplicate template param ' . $template_name . ' in docblock for '
2306
                            . $cased_function_id,
2307
                        new CodeLocation($this->file_scanner, $stmt, null, true)
2308
                    );
2309
                } else {
2310
                    $storage->template_types[$template_name] = [
2311
                        'fn-' . strtolower($cased_function_id) => [$template_type],
2312
                    ];
2313
                }
2314
2315
                $storage->template_covariants[$i] = $template_map[3];
2316
            }
2317
2318
            $template_types = array_merge($template_types ?: [], $storage->template_types);
2319
2320
            $this->function_template_types = $template_types;
2321
        }
2322
2323
        if ($docblock_info->assertions) {
2324
            $storage->assertions = [];
2325
2326
            foreach ($docblock_info->assertions as $assertion) {
2327
                $assertion_type_parts = $this->getAssertionParts(
2328
                    $storage,
2329
                    $assertion['type'],
2330
                    $stmt
2331
                );
2332
2333
                if (!$assertion_type_parts) {
2334
                    continue;
2335
                }
2336
2337
                foreach ($storage->params as $i => $param) {
2338
                    if ($param->name === $assertion['param_name']) {
2339
                        $storage->assertions[] = new \Psalm\Storage\Assertion(
2340
                            $i,
2341
                            [$assertion_type_parts]
2342
                        );
2343
                        continue 2;
2344
                    }
2345
                }
2346
2347
                $storage->assertions[] = new \Psalm\Storage\Assertion(
2348
                    '$' . $assertion['param_name'],
2349
                    [$assertion_type_parts]
2350
                );
2351
            }
2352
        }
2353
2354
        if ($docblock_info->if_true_assertions) {
2355
            $storage->if_true_assertions = [];
2356
2357
            foreach ($docblock_info->if_true_assertions as $assertion) {
2358
                $assertion_type_parts = $this->getAssertionParts(
2359
                    $storage,
2360
                    $assertion['type'],
2361
                    $stmt
2362
                );
2363
2364
                if (!$assertion_type_parts) {
2365
                    continue;
2366
                }
2367
2368
                foreach ($storage->params as $i => $param) {
2369
                    if ($param->name === $assertion['param_name']) {
2370
                        $storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
2371
                            $i,
2372
                            [$assertion_type_parts]
2373
                        );
2374
                        continue 2;
2375
                    }
2376
                }
2377
2378
                $storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
2379
                    '$' . $assertion['param_name'],
2380
                    [$assertion_type_parts]
2381
                );
2382
            }
2383
        }
2384
2385
        if ($docblock_info->if_false_assertions) {
2386
            $storage->if_false_assertions = [];
2387
2388
            foreach ($docblock_info->if_false_assertions as $assertion) {
2389
                $assertion_type_parts = $this->getAssertionParts(
2390
                    $storage,
2391
                    $assertion['type'],
2392
                    $stmt
2393
                );
2394
2395
                if (!$assertion_type_parts) {
2396
                    continue;
2397
                }
2398
2399
                foreach ($storage->params as $i => $param) {
2400
                    if ($param->name === $assertion['param_name']) {
2401
                        $storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
2402
                            $i,
2403
                            [$assertion_type_parts]
2404
                        );
2405
                        continue 2;
2406
                    }
2407
                }
2408
2409
                $storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
2410
                    '$' . $assertion['param_name'],
2411
                    [$assertion_type_parts]
2412
                );
2413
            }
2414
        }
2415
2416
        foreach ($docblock_info->globals as $global) {
2417
            try {
2418
                $storage->global_types[$global['name']] = TypeParser::parseTokens(
2419
                    TypeTokenizer::getFullyQualifiedTokens(
2420
                        $global['type'],
2421
                        $this->aliases,
2422
                        null,
2423
                        $this->type_aliases
2424
                    ),
2425
                    null
2426
                );
2427
            } catch (TypeParseTreeException $e) {
2428
                $storage->docblock_issues[] = new InvalidDocblock(
2429
                    $e->getMessage() . ' in docblock for ' . $cased_function_id,
2430
                    new CodeLocation($this->file_scanner, $stmt, null, true)
2431
                );
2432
2433
                continue;
2434
            }
2435
        }
2436
2437
        if ($docblock_info->params) {
2438
            $this->improveParamsFromDocblock(
2439
                $storage,
2440
                $docblock_info->params,
2441
                $stmt,
2442
                $fake_method,
2443
                $class_storage && !$class_storage->is_trait ? $class_storage->name : null
2444
            );
2445
        }
2446
2447
        if ($storage instanceof MethodStorage) {
2448
            $storage->has_docblock_param_types = (bool) array_filter(
2449
                $storage->params,
2450
                /** @return bool */
2451
                function (FunctionLikeParameter $p) {
2452
                    return $p->type !== null && $p->has_docblock_type;
2453
                }
2454
            );
2455
        }
2456
2457
        $class_template_types = $this->class_template_types;
2458
2459
        foreach ($docblock_info->params_out as $docblock_param_out) {
2460
            $param_name = substr($docblock_param_out['name'], 1);
2461
2462
            try {
2463
                $out_type = TypeParser::parseTokens(
2464
                    TypeTokenizer::getFullyQualifiedTokens(
2465
                        $docblock_param_out['type'],
2466
                        $this->aliases,
2467
                        $this->function_template_types + $class_template_types,
2468
                        $this->type_aliases
2469
                    ),
2470
                    null,
2471
                    $this->function_template_types + $class_template_types,
2472
                    $this->type_aliases
2473
                );
2474
            } catch (TypeParseTreeException $e) {
2475
                $storage->docblock_issues[] = new InvalidDocblock(
2476
                    $e->getMessage() . ' in docblock for ' . $cased_function_id,
2477
                    new CodeLocation($this->file_scanner, $stmt, null, true)
2478
                );
2479
2480
                continue;
2481
            }
2482
2483
            $out_type->queueClassLikesForScanning(
2484
                $this->codebase,
2485
                $this->file_storage,
2486
                $storage->template_types ?: []
2487
            );
2488
2489
            foreach ($storage->params as $i => $param_storage) {
2490
                if ($param_storage->name === $param_name) {
2491
                    $param_storage->out_type = $out_type;
2492
                }
2493
            }
2494
        }
2495
2496
        if ($docblock_info->self_out
2497
            && $storage instanceof MethodStorage) {
2498
            $out_type = TypeParser::parseTokens(
2499
                TypeTokenizer::getFullyQualifiedTokens(
2500
                    $docblock_info->self_out['type'],
2501
                    $this->aliases,
2502
                    $this->function_template_types + $class_template_types,
2503
                    $this->type_aliases
2504
                ),
2505
                null,
2506
                $this->function_template_types + $class_template_types,
2507
                $this->type_aliases
2508
            );
2509
            $storage->self_out_type = $out_type;
2510
        }
2511
2512
        foreach ($docblock_info->taint_sink_params as $taint_sink_param) {
2513
            $param_name = substr($taint_sink_param['name'], 1);
2514
2515
            foreach ($storage->params as $param_storage) {
2516
                if ($param_storage->name === $param_name) {
2517
                    $param_storage->sinks[] = $taint_sink_param['taint'];
2518
                }
2519
            }
2520
        }
2521
2522
        foreach ($docblock_info->taint_source_types as $taint_source_type) {
2523
            if ($taint_source_type === 'input') {
2524
                $storage->taint_source_types = array_merge(
2525
                    $storage->taint_source_types,
2526
                    \Psalm\Type\TaintKindGroup::ALL_INPUT
2527
                );
2528
            } else {
2529
                $storage->taint_source_types[] = $taint_source_type;
2530
            }
2531
        }
2532
2533
        $storage->added_taints = $docblock_info->added_taints;
2534
        $storage->removed_taints = $docblock_info->removed_taints;
2535
2536
        if ($docblock_info->flows) {
2537
            foreach ($docblock_info->flows as $flow) {
2538
                $path_type = 'arg';
2539
2540
                $fancy_path_regex = '/-\(([a-z\-]+)\)->/';
2541
2542
                if (preg_match($fancy_path_regex, $flow, $matches)) {
2543
                    if (isset($matches[1])) {
2544
                        $path_type = $matches[1];
2545
                    }
2546
2547
                    $flow = preg_replace($fancy_path_regex, '->', $flow);
2548
                }
2549
2550
                $flow_parts = explode('->', $flow);
2551
2552
                if (isset($flow_parts[1]) && trim($flow_parts[1]) === 'return') {
2553
                    $source_param_string = trim($flow_parts[0]);
2554
2555
                    if ($source_param_string[0] === '(' && substr($source_param_string, -1) === ')') {
2556
                        $source_params = preg_split('/, ?/', substr($source_param_string, 1, -1));
2557
2558
                        foreach ($source_params as $source_param) {
2559
                            $source_param = substr($source_param, 1);
2560
2561
                            foreach ($storage->params as $i => $param_storage) {
2562
                                if ($param_storage->name === $source_param) {
2563
                                    $storage->return_source_params[$i] = $path_type;
2564
                                }
2565
                            }
2566
                        }
2567
                    }
2568
                }
2569
            }
2570
        }
2571
2572
        foreach ($docblock_info->assert_untainted_params as $untainted_assert_param) {
2573
            $param_name = substr($untainted_assert_param['name'], 1);
2574
2575
            foreach ($storage->params as $param_storage) {
2576
                if ($param_storage->name === $param_name) {
2577
                    $param_storage->assert_untainted = true;
2578
                }
2579
            }
2580
        }
2581
2582
        if ($docblock_info->template_typeofs) {
2583
            foreach ($docblock_info->template_typeofs as $template_typeof) {
2584
                foreach ($storage->params as $param) {
2585
                    if ($param->name === $template_typeof['param_name']) {
2586
                        $param_type_nullable = $param->type && $param->type->isNullable();
2587
2588
                        $template_type = null;
2589
                        $template_class = null;
2590
2591
                        if (isset($template_types[$template_typeof['template_type']])) {
2592
                            foreach ($template_types[$template_typeof['template_type']] as $class => $map) {
2593
                                $template_type = $map[0];
2594
                                $template_class = $class;
2595
                            }
2596
                        }
2597
2598
                        $template_atomic_type = null;
2599
2600
                        if ($template_type) {
2601
                            foreach ($template_type->getAtomicTypes() as $tat) {
2602
                                if ($tat instanceof Type\Atomic\TNamedObject) {
2603
                                    $template_atomic_type = $tat;
2604
                                }
2605
                            }
2606
                        }
2607
2608
                        $param->type = new Type\Union([
2609
                            new Type\Atomic\TTemplateParamClass(
2610
                                $template_typeof['template_type'],
2611
                                $template_type && !$template_type->isMixed()
2612
                                    ? (string)$template_type
2613
                                    : 'object',
2614
                                $template_atomic_type,
2615
                                $template_class ?: 'fn-' . strtolower($cased_function_id)
2616
                            ),
2617
                        ]);
2618
2619
                        if ($param_type_nullable) {
2620
                            $param->type->addType(new Type\Atomic\TNull);
2621
                        }
2622
2623
                        break;
2624
                    }
2625
                }
2626
            }
2627
        }
2628
2629
        if ($docblock_info->return_type) {
2630
            $docblock_return_type = $docblock_info->return_type;
2631
2632
            if (!$fake_method
2633
                && $docblock_info->return_type_line_number
2634
                && $docblock_info->return_type_start
2635
                && $docblock_info->return_type_end
2636
            ) {
2637
                $storage->return_type_location = new CodeLocation\DocblockTypeLocation(
2638
                    $this->file_scanner,
2639
                    $docblock_info->return_type_start,
2640
                    $docblock_info->return_type_end,
2641
                    $docblock_info->return_type_line_number
2642
                );
2643
            } else {
2644
                $storage->return_type_location = new CodeLocation(
2645
                    $this->file_scanner,
2646
                    $stmt,
2647
                    null,
2648
                    false,
2649
                    !$fake_method
2650
                        ? CodeLocation::FUNCTION_PHPDOC_RETURN_TYPE
2651
                        : CodeLocation::FUNCTION_PHPDOC_METHOD,
2652
                    $docblock_info->return_type
2653
                );
2654
            }
2655
2656
            try {
2657
                $fixed_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
2658
                    $docblock_return_type,
2659
                    $this->aliases,
2660
                    $this->function_template_types + $class_template_types,
2661
                    $this->type_aliases,
2662
                    $class_storage && !$class_storage->is_trait ? $class_storage->name : null
2663
                );
2664
2665
                $param_type_mapping = [];
2666
2667
                // This checks for param references in the return type tokens
2668
                // If found, the param is replaced with a generated template param
2669
                foreach ($fixed_type_tokens as $i => $type_token) {
2670
                    $token_body = $type_token[0];
2671
                    $template_function_id = 'fn-' . strtolower($cased_function_id);
2672
2673
                    if ($token_body[0] === '$') {
2674
                        foreach ($storage->params as $j => $param_storage) {
2675
                            if ('$' . $param_storage->name === $token_body) {
2676
                                if (!isset($param_type_mapping[$token_body])) {
2677
                                    $template_name = 'TGeneratedFromParam' . $j;
2678
2679
                                    $template_as_type = $param_storage->type
2680
                                        ? clone $param_storage->type
2681
                                        : Type::getMixed();
2682
2683
                                    $storage->template_types[$template_name] = [
2684
                                        $template_function_id => [
2685
                                            $template_as_type
2686
                                        ],
2687
                                    ];
2688
2689
                                    $this->function_template_types[$template_name]
2690
                                        = $storage->template_types[$template_name];
2691
2692
                                    $param_type_mapping[$token_body] = $template_name;
2693
2694
                                    $param_storage->type = new Type\Union([
2695
                                        new Type\Atomic\TTemplateParam(
2696
                                            $template_name,
2697
                                            $template_as_type,
2698
                                            $template_function_id
2699
                                        )
2700
                                    ]);
2701
                                }
2702
2703
                                // spaces are allowed before $foo in get(string $foo) magic method
2704
                                // definitions, but we want to remove them in this instance
2705
                                if (isset($fixed_type_tokens[$i - 1])
2706
                                    && $fixed_type_tokens[$i - 1][0][0] === ' '
2707
                                ) {
2708
                                    unset($fixed_type_tokens[$i - 1]);
2709
                                }
2710
2711
                                $fixed_type_tokens[$i][0] = $param_type_mapping[$token_body];
2712
2713
                                continue 2;
2714
                            }
2715
                        }
2716
                    }
2717
2718
                    if ($token_body === 'func_num_args()') {
2719
                        $template_name = 'TFunctionArgCount';
2720
2721
                        $storage->template_types[$template_name] = [
2722
                            $template_function_id => [
2723
                                Type::getInt()
2724
                            ],
2725
                        ];
2726
2727
                        $this->function_template_types[$template_name]
2728
                            = $storage->template_types[$template_name];
2729
2730
                        $fixed_type_tokens[$i][0] = $template_name;
2731
                    }
2732
                }
2733
2734
                $storage->return_type = TypeParser::parseTokens(
2735
                    \array_values($fixed_type_tokens),
2736
                    null,
2737
                    $this->function_template_types + $class_template_types,
2738
                    $this->type_aliases
2739
                );
2740
2741
                $storage->return_type->setFromDocblock();
2742
2743
                if ($storage->signature_return_type) {
2744
                    $all_typehint_types_match = true;
2745
                    $signature_return_atomic_types = $storage->signature_return_type->getAtomicTypes();
2746
2747
                    foreach ($storage->return_type->getAtomicTypes() as $key => $type) {
2748
                        if (isset($signature_return_atomic_types[$key])) {
2749
                            $type->from_docblock = false;
2750
                        } else {
2751
                            $all_typehint_types_match = false;
2752
                        }
2753
                    }
2754
2755
                    if ($all_typehint_types_match) {
2756
                        $storage->return_type->from_docblock = false;
2757
                    }
2758
2759
                    if ($storage->signature_return_type->isNullable()
2760
                        && !$storage->return_type->isNullable()
2761
                        && !$storage->return_type->hasTemplate()
2762
                        && !$storage->return_type->hasConditional()
2763
                    ) {
2764
                        $storage->return_type->addType(new Type\Atomic\TNull());
2765
                    }
2766
                }
2767
2768
                $storage->return_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
2769
            } catch (TypeParseTreeException $e) {
2770
                $storage->docblock_issues[] = new InvalidDocblock(
2771
                    $e->getMessage() . ' in docblock for ' . $cased_function_id,
2772
                    new CodeLocation($this->file_scanner, $stmt, null, true)
2773
                );
2774
            }
2775
2776
            if ($storage->return_type && $docblock_info->ignore_nullable_return) {
2777
                $storage->return_type->ignore_nullable_issues = true;
2778
            }
2779
2780
            if ($storage->return_type && $docblock_info->ignore_falsable_return) {
2781
                $storage->return_type->ignore_falsable_issues = true;
2782
            }
2783
2784
            if ($stmt->returnsByRef() && $storage->return_type) {
2785
                $storage->return_type->by_ref = true;
2786
            }
2787
2788
            if ($docblock_info->return_type_line_number && !$fake_method) {
2789
                $storage->return_type_location->setCommentLine($docblock_info->return_type_line_number);
2790
            }
2791
2792
            $storage->return_type_description = $docblock_info->return_type_description;
2793
        }
2794
2795
        if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
2796
            && $stmt->name->name === '__construct'
2797
            && $class_storage
2798
            && $storage instanceof MethodStorage
2799
            && $storage->params
2800
            && $this->config->infer_property_types_from_constructor
2801
        ) {
2802
            $this->inferPropertyTypeFromConstructor($stmt, $storage, $class_storage);
2803
        }
2804
2805
        return $storage;
2806
    }
2807
2808
    private function inferPropertyTypeFromConstructor(
2809
        PhpParser\Node\Stmt\ClassMethod $stmt,
2810
        MethodStorage $storage,
2811
        ClassLikeStorage $class_storage
2812
    ) : void {
2813
        if (!$stmt->stmts) {
2814
            return;
2815
        }
2816
2817
        $assigned_properties = [];
2818
2819
        foreach ($stmt->stmts as $function_stmt) {
2820
            if ($function_stmt instanceof PhpParser\Node\Stmt\Expression
2821
                && $function_stmt->expr instanceof PhpParser\Node\Expr\Assign
2822
                && $function_stmt->expr->var instanceof PhpParser\Node\Expr\PropertyFetch
2823
                && $function_stmt->expr->var->var instanceof PhpParser\Node\Expr\Variable
2824
                && $function_stmt->expr->var->var->name === 'this'
2825
                && $function_stmt->expr->var->name instanceof PhpParser\Node\Identifier
2826
                && ($property_name = $function_stmt->expr->var->name->name)
2827
                && isset($class_storage->properties[$property_name])
2828
                && $function_stmt->expr->expr instanceof PhpParser\Node\Expr\Variable
2829
                && is_string($function_stmt->expr->expr->name)
2830
                && ($param_name = $function_stmt->expr->expr->name)
2831
                && isset($storage->param_lookup[$param_name])
2832
            ) {
2833
                if ($class_storage->properties[$property_name]->type
2834
                    || !$storage->param_lookup[$param_name]
2835
                ) {
2836
                    continue;
2837
                }
2838
2839
                $param_index = \array_search($param_name, \array_keys($storage->param_lookup), true);
2840
2841
                if ($param_index === false || !isset($storage->params[$param_index]->type)) {
2842
                    continue;
2843
                }
2844
2845
                $param_type = $storage->params[$param_index]->type;
2846
2847
                $assigned_properties[$property_name] =
2848
                    $storage->params[$param_index]->is_variadic
2849
                        ? new Type\Union([
2850
                            new Type\Atomic\TArray([
2851
                                Type::getInt(),
2852
                                $param_type,
2853
                            ]),
2854
                        ])
2855
                        : $param_type;
2856
            } else {
2857
                $assigned_properties = [];
2858
                break;
2859
            }
2860
        }
2861
2862
        if (!$assigned_properties) {
2863
            return;
2864
        }
2865
2866
        $storage->external_mutation_free = true;
2867
2868
        foreach ($assigned_properties as $property_name => $property_type) {
2869
            $class_storage->properties[$property_name]->type = clone $property_type;
2870
        }
2871
    }
2872
2873
    /**
2874
     * @return ?list<string>
0 ignored issues
show
Documentation introduced by
The doc-type ?list<string> could not be parsed: Unknown type name "?list" 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...
2875
     */
2876
    private function getAssertionParts(
2877
        FunctionLikeStorage $storage,
2878
        string $assertion_type,
2879
        PhpParser\Node\FunctionLike $stmt
2880
    ) : ?array {
2881
        $prefix = '';
2882
2883
        if ($assertion_type[0] === '!') {
2884
            $prefix = '!';
2885
            $assertion_type = substr($assertion_type, 1);
2886
        }
2887
2888
        if ($assertion_type[0] === '~') {
2889
            $prefix .= '~';
2890
            $assertion_type = substr($assertion_type, 1);
2891
        }
2892
2893
        if ($assertion_type[0] === '=') {
2894
            $prefix .= '=';
2895
            $assertion_type = substr($assertion_type, 1);
2896
        }
2897
2898
        $class_template_types = !$stmt instanceof PhpParser\Node\Stmt\ClassMethod || !$stmt->isStatic()
2899
            ? $this->class_template_types
2900
            : [];
2901
2902
        try {
2903
            $namespaced_type = TypeParser::parseTokens(
2904
                TypeTokenizer::getFullyQualifiedTokens(
2905
                    $assertion_type,
2906
                    $this->aliases,
2907
                    $this->function_template_types + $class_template_types,
2908
                    $this->type_aliases,
2909
                    null,
2910
                    null,
2911
                    true
2912
                )
2913
            );
2914
        } catch (TypeParseTreeException $e) {
2915
            $storage->docblock_issues[] = new InvalidDocblock(
2916
                'Invalid @psalm-assert union type ' . $e,
2917
                new CodeLocation($this->file_scanner, $stmt, null, true)
2918
            );
2919
2920
            return null;
2921
        }
2922
2923
2924
        if ($prefix && count($namespaced_type->getAtomicTypes()) > 1) {
2925
            $storage->docblock_issues[] = new InvalidDocblock(
2926
                'Docblock assertions cannot contain | characters together with ' . $prefix,
2927
                new CodeLocation($this->file_scanner, $stmt, null, true)
2928
            );
2929
2930
            return null;
2931
        }
2932
2933
        $namespaced_type->queueClassLikesForScanning(
2934
            $this->codebase,
2935
            $this->file_storage,
2936
            $this->function_template_types + $class_template_types
2937
        );
2938
2939
        $assertion_type_parts = [];
2940
2941
        foreach ($namespaced_type->getAtomicTypes() as $namespaced_type_part) {
2942
            if ($namespaced_type_part instanceof Type\Atomic\TAssertionFalsy
2943
                || ($namespaced_type_part instanceof Type\Atomic\TList
2944
                    && !$namespaced_type_part instanceof Type\Atomic\TNonEmptyList
2945
                    && $namespaced_type_part->type_param->isMixed())
2946
                || ($namespaced_type_part instanceof Type\Atomic\TArray
2947
                    && $namespaced_type_part->type_params[0]->isArrayKey()
2948
                    && $namespaced_type_part->type_params[1]->isMixed())
2949
                || ($namespaced_type_part instanceof Type\Atomic\TIterable
2950
                    && $namespaced_type_part->type_params[0]->isMixed()
2951
                    && $namespaced_type_part->type_params[1]->isMixed())
2952
            ) {
2953
                $assertion_type_parts[] = $prefix . $namespaced_type_part->getAssertionString();
2954
            } else {
2955
                $assertion_type_parts[] = $prefix . $namespaced_type_part->getId();
2956
            }
2957
        }
2958
2959
        return $assertion_type_parts;
2960
    }
2961
2962
    /**
2963
     * @param  PhpParser\Node\Param $param
2964
     *
2965
     * @return FunctionLikeParameter
2966
     */
2967
    public function getTranslatedFunctionParam(
2968
        PhpParser\Node\Param $param,
2969
        PhpParser\Node\FunctionLike $stmt,
2970
        bool $fake_method,
2971
        ?string $fq_classlike_name
2972
    ) : FunctionLikeParameter {
2973
        $param_type = null;
2974
2975
        $is_nullable = $param->default !== null &&
2976
            $param->default instanceof PhpParser\Node\Expr\ConstFetch &&
2977
            strtolower($param->default->name->parts[0]) === 'null';
2978
2979
        $param_typehint = $param->type;
2980
2981
        if ($param_typehint instanceof PhpParser\Node\NullableType) {
2982
            $is_nullable = true;
2983
            $param_typehint = $param_typehint->type;
2984
        }
2985
2986
        if ($param_typehint) {
2987
            if ($param_typehint instanceof PhpParser\Node\Identifier) {
2988
                $param_type_string = $param_typehint->name;
2989
            } elseif ($param_typehint instanceof PhpParser\Node\Name\FullyQualified) {
2990
                $param_type_string = (string)$param_typehint;
2991
2992
                $this->codebase->scanner->queueClassLikeForScanning($param_type_string);
2993
                $this->file_storage->referenced_classlikes[strtolower($param_type_string)] = $param_type_string;
2994
            } elseif ($param_typehint instanceof PhpParser\Node\UnionType) {
2995
                // not yet supported
2996
                $param_type_string = 'mixed';
2997
            } else {
2998
                if ($this->classlike_storages
2999
                    && strtolower($param_typehint->parts[0]) === 'self'
3000
                    && !end($this->classlike_storages)->is_trait
3001
                ) {
3002
                    $param_type_string = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
3003
                } else {
3004
                    $param_type_string = ClassLikeAnalyzer::getFQCLNFromNameObject($param_typehint, $this->aliases);
3005
                }
3006
3007
                if (!in_array(strtolower($param_type_string), ['self', 'static', 'parent'], true)) {
3008
                    $this->codebase->scanner->queueClassLikeForScanning($param_type_string);
3009
                    $this->file_storage->referenced_classlikes[strtolower($param_type_string)] = $param_type_string;
3010
                }
3011
            }
3012
3013
            if ($param_type_string) {
3014
                $param_type = Type::parseString(
3015
                    $param_type_string,
3016
                    [$this->php_major_version, $this->php_minor_version],
3017
                    []
3018
                );
3019
3020
                if ($is_nullable) {
3021
                    $param_type->addType(new Type\Atomic\TNull);
3022
                }
3023
            }
3024
        }
3025
3026
        $is_optional = $param->default !== null;
3027
3028
        if ($param->var instanceof PhpParser\Node\Expr\Error || !is_string($param->var->name)) {
3029
            throw new \UnexpectedValueException('Not expecting param name to be non-string');
3030
        }
3031
3032
        return new FunctionLikeParameter(
3033
            $param->var->name,
3034
            $param->byRef,
3035
            $param_type,
3036
            new CodeLocation(
3037
                $this->file_scanner,
3038
                $fake_method ? $stmt : $param,
3039
                null,
3040
                false,
3041
                !$fake_method
3042
                    ? CodeLocation::FUNCTION_PARAM_VAR
3043
                    : CodeLocation::FUNCTION_PHPDOC_METHOD
3044
            ),
3045
            $param_typehint
3046
                ? new CodeLocation(
3047
                    $this->file_scanner,
3048
                    $fake_method ? $stmt : $param,
3049
                    null,
3050
                    false,
3051
                    CodeLocation::FUNCTION_PARAM_TYPE
3052
                )
3053
                : null,
3054
            $is_optional,
3055
            $is_nullable,
3056
            $param->variadic,
3057
            $param->default
3058
                ? SimpleTypeInferer::infer(
3059
                    $this->codebase,
3060
                    new \Psalm\Internal\Provider\NodeDataProvider(),
3061
                    $param->default,
3062
                    $this->aliases,
3063
                    null,
3064
                    null,
3065
                    $fq_classlike_name
3066
                )
3067
                : null
3068
        );
3069
    }
3070
3071
    /**
3072
     * @param  FunctionLikeStorage          $storage
3073
     * @param  array<int, array{type:string,name:string,line_number:int,start:int,end:int}>  $docblock_params
3074
     * @param  PhpParser\Node\FunctionLike  $function
3075
     *
3076
     * @return void
3077
     */
3078
    private function improveParamsFromDocblock(
3079
        FunctionLikeStorage $storage,
3080
        array $docblock_params,
3081
        PhpParser\Node\FunctionLike $function,
3082
        bool $fake_method,
3083
        ?string $fq_classlike_name
3084
    ) {
3085
        $base = $this->fq_classlike_names
3086
            ? $this->fq_classlike_names[count($this->fq_classlike_names) - 1] . '::'
3087
            : '';
3088
3089
        $cased_method_id = $base . $storage->cased_name;
3090
3091
        $unused_docblock_params = [];
3092
3093
        $class_template_types = !$function instanceof PhpParser\Node\Stmt\ClassMethod || !$function->isStatic()
3094
            ? $this->class_template_types
3095
            : [];
3096
3097
        foreach ($docblock_params as $docblock_param) {
3098
            $param_name = $docblock_param['name'];
3099
            $docblock_param_variadic = false;
3100
3101
            if (substr($param_name, 0, 3) === '...') {
3102
                $docblock_param_variadic = true;
3103
                $param_name = substr($param_name, 3);
3104
            }
3105
3106
            $param_name = substr($param_name, 1);
3107
3108
            $storage_param = null;
3109
3110
            foreach ($storage->params as $function_signature_param) {
3111
                if ($function_signature_param->name === $param_name) {
3112
                    $storage_param = $function_signature_param;
3113
                    break;
3114
                }
3115
            }
3116
3117
            if (!$fake_method) {
3118
                $docblock_type_location = new CodeLocation\DocblockTypeLocation(
3119
                    $this->file_scanner,
3120
                    $docblock_param['start'],
3121
                    $docblock_param['end'],
3122
                    $docblock_param['line_number']
3123
                );
3124
            } else {
3125
                $docblock_type_location = new CodeLocation(
3126
                    $this->file_scanner,
3127
                    $function,
3128
                    null,
3129
                    false,
3130
                    CodeLocation::FUNCTION_PHPDOC_METHOD,
3131
                    null
3132
                );
3133
            }
3134
3135
            if ($storage_param === null) {
3136
                $param_location = new CodeLocation(
3137
                    $this->file_scanner,
3138
                    $function,
3139
                    null,
3140
                    true,
3141
                    CodeLocation::FUNCTION_PARAM_VAR
3142
                );
3143
3144
                $param_location->setCommentLine($docblock_param['line_number']);
3145
                $unused_docblock_params[$param_name] = $param_location;
3146
3147
                if (!$docblock_param_variadic || $storage->params || $this->scan_deep) {
3148
                    continue;
3149
                }
3150
3151
                $storage_param = new FunctionLikeParameter(
3152
                    $param_name,
3153
                    false,
3154
                    null,
3155
                    null,
3156
                    null,
3157
                    false,
3158
                    false,
3159
                    true,
3160
                    null
3161
                );
3162
3163
                $storage->params[] = $storage_param;
3164
            }
3165
3166
            try {
3167
                $new_param_type = TypeParser::parseTokens(
3168
                    TypeTokenizer::getFullyQualifiedTokens(
3169
                        $docblock_param['type'],
3170
                        $this->aliases,
3171
                        $this->function_template_types + $class_template_types,
3172
                        $this->type_aliases,
3173
                        $fq_classlike_name
3174
                    ),
3175
                    null,
3176
                    $this->function_template_types + $class_template_types,
3177
                    $this->type_aliases
3178
                );
3179
            } catch (TypeParseTreeException $e) {
3180
                $storage->docblock_issues[] = new InvalidDocblock(
3181
                    $e->getMessage() . ' in docblock for ' . $cased_method_id,
3182
                    $docblock_type_location
3183
                );
3184
3185
                continue;
3186
            }
3187
3188
            $storage_param->has_docblock_type = true;
3189
            $new_param_type->setFromDocblock();
3190
3191
            $new_param_type->queueClassLikesForScanning(
3192
                $this->codebase,
3193
                $this->file_storage,
3194
                $storage->template_types ?: []
3195
            );
3196
3197
            if ($storage->template_types) {
3198
                foreach ($storage->template_types as $t => $type_map) {
3199
                    foreach ($type_map as $obj => [$type]) {
3200
                        if ($type->isMixed() && $docblock_param['type'] === 'class-string<' . $t . '>') {
0 ignored issues
show
Bug introduced by
The variable $type seems to be defined by a foreach iteration on line 3254. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
3201
                            $storage->template_types[$t][$obj] = [Type::getObject()];
3202
3203
                            if (isset($this->function_template_types[$t])) {
3204
                                $this->function_template_types[$t][$obj] = $storage->template_types[$t][$obj];
3205
                            }
3206
                        }
3207
                    }
3208
                }
3209
            }
3210
3211
            if (!$docblock_param_variadic && $storage_param->is_variadic && $new_param_type->hasArray()) {
3212
                /**
3213
                 * @psalm-suppress PossiblyUndefinedStringArrayOffset
3214
                 * @var Type\Atomic\TArray|Type\Atomic\ObjectLike|Type\Atomic\TList
3215
                 */
3216
                $array_type = $new_param_type->getAtomicTypes()['array'];
3217
3218
                if ($array_type instanceof Type\Atomic\ObjectLike) {
3219
                    $new_param_type = $array_type->getGenericValueType();
3220
                } elseif ($array_type instanceof Type\Atomic\TList) {
3221
                    $new_param_type = $array_type->type_param;
3222
                } else {
3223
                    $new_param_type = $array_type->type_params[1];
3224
                }
3225
            }
3226
3227
            $existing_param_type_nullable = $storage_param->is_nullable;
3228
3229
            if (!$storage_param->type || $storage_param->type->hasMixed() || $storage->template_types) {
3230
                if ($existing_param_type_nullable
3231
                    && !$new_param_type->isNullable()
3232
                    && !$new_param_type->hasTemplate()
3233
                ) {
3234
                    $new_param_type->addType(new Type\Atomic\TNull());
3235
                }
3236
3237
                if ($this->config->add_param_default_to_docblock_type
3238
                    && $storage_param->default_type
3239
                    && !$storage_param->default_type->hasMixed()
3240
                    && (!$storage_param->type || !$storage_param->type->hasMixed())
3241
                ) {
3242
                    $new_param_type = Type::combineUnionTypes($new_param_type, $storage_param->default_type);
3243
                }
3244
3245
                $storage_param->type = $new_param_type;
3246
                $storage_param->type_location = $docblock_type_location;
3247
                continue;
3248
            }
3249
3250
            $storage_param_atomic_types = $storage_param->type->getAtomicTypes();
3251
3252
            $all_typehint_types_match = true;
3253
3254
            foreach ($new_param_type->getAtomicTypes() as $key => $type) {
3255
                if (isset($storage_param_atomic_types[$key])) {
3256
                    $type->from_docblock = false;
3257
3258
                    if ($storage_param_atomic_types[$key] instanceof Type\Atomic\TArray
3259
                        && $type instanceof Type\Atomic\TArray
3260
                        && $type->type_params[0]->hasArrayKey()
3261
                    ) {
3262
                        $type->type_params[0]->from_docblock = false;
3263
                    }
3264
                } else {
3265
                    $all_typehint_types_match = false;
3266
                }
3267
            }
3268
3269
            if ($all_typehint_types_match) {
3270
                $new_param_type->from_docblock = false;
3271
            }
3272
3273
            if ($existing_param_type_nullable && !$new_param_type->isNullable()) {
3274
                $new_param_type->addType(new Type\Atomic\TNull());
3275
            }
3276
3277
            $storage_param->type = $new_param_type;
3278
            $storage_param->type_location = $docblock_type_location;
3279
        }
3280
3281
        $params_without_docblock_type = array_filter(
3282
            $storage->params,
3283
            function (FunctionLikeParameter $p) : bool {
3284
                return !$p->has_docblock_type && (!$p->type || $p->type->hasArray());
3285
            }
3286
        );
3287
3288
        if ($params_without_docblock_type) {
3289
            $storage->unused_docblock_params = $unused_docblock_params;
3290
        }
3291
    }
3292
3293
    /**
3294
     * @param   PhpParser\Node\Stmt\Property    $stmt
3295
     * @param   Config                          $config
3296
     * @param   string                          $fq_classlike_name
3297
     *
3298
     * @return  void
3299
     */
3300
    private function visitPropertyDeclaration(
3301
        PhpParser\Node\Stmt\Property $stmt,
3302
        Config $config,
3303
        ClassLikeStorage $storage,
3304
        $fq_classlike_name
3305
    ) {
3306
        if (!$this->fq_classlike_names) {
3307
            throw new \LogicException('$this->fq_classlike_names should not be empty');
3308
        }
3309
3310
        $comment = $stmt->getDocComment();
3311
        $var_comment = null;
3312
3313
        $property_is_initialized = false;
3314
3315
        $existing_constants = $storage->protected_class_constants
3316
            + $storage->private_class_constants
3317
            + $storage->public_class_constants;
3318
3319
        if ($comment && $comment->getText() && ($config->use_docblock_types || $config->use_docblock_property_types)) {
3320
            if (preg_match('/[ \t\*]+@psalm-suppress[ \t]+PropertyNotSetInConstructor/', (string)$comment)) {
3321
                $property_is_initialized = true;
3322
            }
3323
3324
            try {
3325
                $var_comments = CommentAnalyzer::getTypeFromComment(
3326
                    $comment,
3327
                    $this->file_scanner,
3328
                    $this->aliases,
3329
                    $this->function_template_types + (!$stmt->isStatic() ? $this->class_template_types : []),
3330
                    $this->type_aliases
3331
                );
3332
3333
                $var_comment = array_pop($var_comments);
3334
            } catch (IncorrectDocblockException $e) {
3335
                $storage->docblock_issues[] = new MissingDocblockType(
3336
                    $e->getMessage(),
3337
                    new CodeLocation($this->file_scanner, $stmt, null, true)
3338
                );
3339
            } catch (DocblockParseException $e) {
3340
                $storage->docblock_issues[] = new InvalidDocblock(
3341
                    $e->getMessage(),
3342
                    new CodeLocation($this->file_scanner, $stmt, null, true)
3343
                );
3344
            }
3345
        }
3346
3347
        $signature_type = null;
3348
        $signature_type_location = null;
3349
3350
        if ($stmt->type) {
3351
            $suffix = '';
3352
3353
            $parser_property_type = $stmt->type;
3354
3355
            if ($parser_property_type instanceof PhpParser\Node\NullableType) {
3356
                $suffix = '|null';
3357
                $parser_property_type = $parser_property_type->type;
3358
            }
3359
3360
            if ($parser_property_type instanceof PhpParser\Node\Identifier) {
3361
                $property_type_string = $parser_property_type->name . $suffix;
3362
            } elseif ($parser_property_type instanceof PhpParser\Node\UnionType) {
3363
                // not yet supported
3364
                $property_type_string = 'mixed';
3365
            } else {
3366
                $property_type_fq_classlike_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
3367
                    $parser_property_type,
3368
                    $this->aliases
3369
                );
3370
3371
                $property_type_string = $property_type_fq_classlike_name . $suffix;
3372
            }
3373
3374
            $signature_type = Type::parseString(
3375
                $property_type_string,
3376
                [$this->php_major_version, $this->php_minor_version]
3377
            );
3378
            $signature_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
3379
3380
            $signature_type_location = new CodeLocation(
3381
                $this->file_scanner,
3382
                $parser_property_type,
3383
                null,
3384
                false,
3385
                CodeLocation::FUNCTION_RETURN_TYPE
3386
            );
3387
        }
3388
3389
        $doc_var_group_type = $var_comment ? $var_comment->type : null;
3390
3391
        if ($doc_var_group_type) {
3392
            $doc_var_group_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
3393
            $doc_var_group_type->setFromDocblock();
3394
        }
3395
3396
        foreach ($stmt->props as $property) {
3397
            $doc_var_location = null;
3398
3399
            $property_storage = $storage->properties[$property->name->name] = new PropertyStorage();
3400
            $property_storage->is_static = $stmt->isStatic();
3401
            $property_storage->type = $signature_type;
3402
            $property_storage->signature_type = $signature_type;
3403
            $property_storage->signature_type_location = $signature_type_location;
3404
            $property_storage->type_location = $signature_type_location;
3405
            $property_storage->location = new CodeLocation($this->file_scanner, $property->name);
3406
            $property_storage->stmt_location = new CodeLocation($this->file_scanner, $stmt);
3407
            $property_storage->has_default = $property->default ? true : false;
3408
            $property_storage->deprecated = $var_comment ? $var_comment->deprecated : false;
3409
            $property_storage->internal = $var_comment ? $var_comment->internal : false;
3410
            $property_storage->psalm_internal = $var_comment ? $var_comment->psalm_internal : null;
3411
            $property_storage->readonly = $var_comment ? $var_comment->readonly : false;
3412
            $property_storage->allow_private_mutation = $var_comment ? $var_comment->allow_private_mutation : false;
3413
3414
            if (!$signature_type && !$doc_var_group_type) {
3415
                if ($property->default) {
3416
                    $property_storage->suggested_type = SimpleTypeInferer::infer(
3417
                        $this->codebase,
3418
                        new \Psalm\Internal\Provider\NodeDataProvider(),
3419
                        $property->default,
3420
                        $this->aliases,
3421
                        null,
3422
                        $existing_constants,
3423
                        $fq_classlike_name
3424
                    );
3425
                }
3426
3427
                $property_storage->type = null;
3428
            } else {
3429
                if ($var_comment
3430
                    && $var_comment->type_start
3431
                    && $var_comment->type_end
3432
                    && $var_comment->line_number
3433
                ) {
3434
                    $doc_var_location = new CodeLocation\DocblockTypeLocation(
3435
                        $this->file_scanner,
3436
                        $var_comment->type_start,
3437
                        $var_comment->type_end,
3438
                        $var_comment->line_number
3439
                    );
3440
                }
3441
3442
                if ($doc_var_group_type) {
3443
                    $property_storage->type = count($stmt->props) === 1
3444
                        ? $doc_var_group_type
3445
                        : clone $doc_var_group_type;
3446
                }
3447
            }
3448
3449
            if ($property_storage->type
3450
                && $property_storage->type !== $property_storage->signature_type
3451
            ) {
3452
                if (!$property_storage->signature_type) {
3453
                    $property_storage->type_location = $doc_var_location;
3454
                }
3455
3456
                if ($property_storage->signature_type) {
3457
                    $all_typehint_types_match = true;
3458
                    $signature_atomic_types = $property_storage->signature_type->getAtomicTypes();
3459
3460
                    foreach ($property_storage->type->getAtomicTypes() as $key => $type) {
3461
                        if (isset($signature_atomic_types[$key])) {
3462
                            $type->from_docblock = false;
3463
                        } else {
3464
                            $all_typehint_types_match = false;
3465
                        }
3466
                    }
3467
3468
                    if ($all_typehint_types_match) {
3469
                        $property_storage->type->from_docblock = false;
3470
                    }
3471
3472
                    if ($property_storage->signature_type->isNullable()
3473
                        && !$property_storage->type->isNullable()
3474
                    ) {
3475
                        $property_storage->type->addType(new Type\Atomic\TNull());
3476
                    }
3477
                }
3478
3479
                $property_storage->type->queueClassLikesForScanning($this->codebase, $this->file_storage);
3480
            }
3481
3482
            if ($stmt->isPublic()) {
3483
                $property_storage->visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC;
3484
            } elseif ($stmt->isProtected()) {
3485
                $property_storage->visibility = ClassLikeAnalyzer::VISIBILITY_PROTECTED;
3486
            } elseif ($stmt->isPrivate()) {
3487
                $property_storage->visibility = ClassLikeAnalyzer::VISIBILITY_PRIVATE;
3488
            }
3489
3490
            $fq_classlike_name = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
3491
3492
            $property_id = $fq_classlike_name . '::$' . $property->name->name;
3493
3494
            $storage->declaring_property_ids[$property->name->name] = $fq_classlike_name;
3495
            $storage->appearing_property_ids[$property->name->name] = $property_id;
3496
3497
            if ($property_is_initialized) {
3498
                $storage->initialized_properties[$property->name->name] = true;
3499
            }
3500
3501
            if (!$stmt->isPrivate()) {
3502
                $storage->inheritable_property_ids[$property->name->name] = $property_id;
3503
            }
3504
        }
3505
    }
3506
3507
    /**
3508
     * @param   PhpParser\Node\Stmt\ClassConst  $stmt
3509
     * @param   string $fq_classlike_name
3510
     *
3511
     * @return  void
3512
     */
3513
    private function visitClassConstDeclaration(
3514
        PhpParser\Node\Stmt\ClassConst $stmt,
3515
        ClassLikeStorage $storage,
3516
        $fq_classlike_name
3517
    ) {
3518
        $existing_constants = $storage->protected_class_constants
3519
            + $storage->private_class_constants
3520
            + $storage->public_class_constants;
3521
3522
        $comment = $stmt->getDocComment();
3523
        $deprecated = false;
3524
        $config = $this->config;
3525
3526
        if ($comment && $comment->getText() && ($config->use_docblock_types || $config->use_docblock_property_types)) {
3527
            $comments = DocComment::parsePreservingLength($comment);
3528
3529
            if (isset($comments->tags['deprecated'])) {
3530
                $deprecated = true;
3531
            }
3532
        }
3533
3534
        foreach ($stmt->consts as $const) {
3535
            $const_type = SimpleTypeInferer::infer(
3536
                $this->codebase,
3537
                new \Psalm\Internal\Provider\NodeDataProvider(),
3538
                $const->value,
3539
                $this->aliases,
3540
                null,
3541
                $existing_constants,
3542
                $fq_classlike_name
3543
            );
3544
3545
            if ($const_type) {
3546
                $existing_constants[$const->name->name] = $const_type;
3547
3548
                if ($stmt->isProtected()) {
3549
                    $storage->protected_class_constants[$const->name->name] = $const_type;
3550
                } elseif ($stmt->isPrivate()) {
3551
                    $storage->private_class_constants[$const->name->name] = $const_type;
3552
                } else {
3553
                    $storage->public_class_constants[$const->name->name] = $const_type;
3554
                }
3555
3556
                $storage->class_constant_locations[$const->name->name] = new CodeLocation(
3557
                    $this->file_scanner,
3558
                    $const->name
3559
                );
3560
3561
                $storage->class_constant_stmt_locations[$const->name->name] = new CodeLocation(
3562
                    $this->file_scanner,
3563
                    $const
3564
                );
3565
            } else {
3566
                $unresolved_const_expr = self::getUnresolvedClassConstExpr(
3567
                    $const->value,
3568
                    $this->aliases,
3569
                    $fq_classlike_name
3570
                );
3571
3572
                if ($stmt->isProtected()) {
3573
                    if ($unresolved_const_expr) {
3574
                        $storage->protected_class_constant_nodes[$const->name->name] = $unresolved_const_expr;
3575
                    } else {
3576
                        $storage->protected_class_constants[$const->name->name] = Type::getMixed();
3577
                    }
3578
                } elseif ($stmt->isPrivate()) {
3579
                    if ($unresolved_const_expr) {
3580
                        $storage->private_class_constant_nodes[$const->name->name] = $unresolved_const_expr;
3581
                    } else {
3582
                        $storage->private_class_constants[$const->name->name] = Type::getMixed();
3583
                    }
3584
                } else {
3585
                    if ($unresolved_const_expr) {
3586
                        $storage->public_class_constant_nodes[$const->name->name] = $unresolved_const_expr;
3587
                    } else {
3588
                        $storage->public_class_constants[$const->name->name] = Type::getMixed();
3589
                    }
3590
                }
3591
            }
3592
3593
            if ($deprecated) {
3594
                $storage->deprecated_constants[$const->name->name] = true;
3595
            }
3596
        }
3597
    }
3598
3599
    public function getUnresolvedClassConstExpr(
3600
        PhpParser\Node\Expr $stmt,
3601
        Aliases $aliases,
3602
        string $fq_classlike_name
3603
    ) : ?UnresolvedConstantComponent {
3604
        if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
3605
            $left = self::getUnresolvedClassConstExpr(
3606
                $stmt->left,
3607
                $aliases,
3608
                $fq_classlike_name
3609
            );
3610
3611
            $right = self::getUnresolvedClassConstExpr(
3612
                $stmt->right,
3613
                $aliases,
3614
                $fq_classlike_name
3615
            );
3616
3617
            if (!$left || !$right) {
3618
                return null;
3619
            }
3620
3621
            if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Plus) {
3622
                return new UnresolvedConstant\UnresolvedAdditionOp($left, $right);
3623
            }
3624
3625
            if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Minus) {
3626
                return new UnresolvedConstant\UnresolvedSubtractionOp($left, $right);
3627
            }
3628
3629
            if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Mul) {
3630
                return new UnresolvedConstant\UnresolvedMultiplicationOp($left, $right);
3631
            }
3632
3633
            if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Div) {
3634
                return new UnresolvedConstant\UnresolvedDivisionOp($left, $right);
3635
            }
3636
3637
            if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
3638
                return new UnresolvedConstant\UnresolvedConcatOp($left, $right);
3639
            }
3640
3641
            if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr) {
3642
                return new UnresolvedConstant\UnresolvedBitwiseOr($left, $right);
3643
            }
3644
        }
3645
3646
        if ($stmt instanceof PhpParser\Node\Expr\Ternary) {
3647
            $cond = self::getUnresolvedClassConstExpr(
3648
                $stmt->cond,
3649
                $aliases,
3650
                $fq_classlike_name
3651
            );
3652
3653
            $if = null;
3654
3655
            if ($stmt->if) {
3656
                $if = self::getUnresolvedClassConstExpr(
3657
                    $stmt->if,
3658
                    $aliases,
3659
                    $fq_classlike_name
3660
                );
3661
3662
                if ($if === null) {
3663
                    $if = false;
3664
                }
3665
            }
3666
3667
            $else = self::getUnresolvedClassConstExpr(
3668
                $stmt->else,
3669
                $aliases,
3670
                $fq_classlike_name
3671
            );
3672
3673
            if ($cond && $else && $if !== false) {
3674
                return new UnresolvedConstant\UnresolvedTernary($cond, $if, $else);
3675
            }
3676
        }
3677
3678
        if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) {
3679
            if (strtolower($stmt->name->parts[0]) === 'false') {
3680
                return new UnresolvedConstant\ScalarValue(false);
3681
            } elseif (strtolower($stmt->name->parts[0]) === 'true') {
3682
                return new UnresolvedConstant\ScalarValue(true);
3683
            } elseif (strtolower($stmt->name->parts[0]) === 'null') {
3684
                return new UnresolvedConstant\ScalarValue(null);
3685
            } elseif ($stmt->name->parts[0] === '__NAMESPACE__') {
3686
                return new UnresolvedConstant\ScalarValue($aliases->namespace);
3687
            }
3688
3689
            return new UnresolvedConstant\Constant(
3690
                implode('\\', $stmt->name->parts),
3691
                $stmt->name instanceof PhpParser\Node\Name\FullyQualified
3692
            );
3693
        }
3694
3695
        if ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Namespace_) {
3696
            return new UnresolvedConstant\ScalarValue($aliases->namespace);
3697
        }
3698
3699
        if ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch && $stmt->dim) {
3700
            $left = self::getUnresolvedClassConstExpr(
3701
                $stmt->var,
3702
                $aliases,
3703
                $fq_classlike_name
3704
            );
3705
3706
            $right = self::getUnresolvedClassConstExpr(
3707
                $stmt->dim,
3708
                $aliases,
3709
                $fq_classlike_name
3710
            );
3711
3712
            if ($left && $right) {
3713
                return new UnresolvedConstant\ArrayOffsetFetch($left, $right);
3714
            }
3715
        }
3716
3717
        if ($stmt instanceof PhpParser\Node\Expr\ClassConstFetch) {
3718
            if ($stmt->class instanceof PhpParser\Node\Name
3719
                && $stmt->name instanceof PhpParser\Node\Identifier
3720
                && $fq_classlike_name
3721
                && $stmt->class->parts !== ['static']
3722
                && $stmt->class->parts !== ['parent']
3723
            ) {
3724
                if ($stmt->class->parts === ['self']) {
3725
                    $const_fq_class_name = $fq_classlike_name;
3726
                } else {
3727
                    $const_fq_class_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
3728
                        $stmt->class,
3729
                        $aliases
3730
                    );
3731
                }
3732
3733
                return new UnresolvedConstant\ClassConstant($const_fq_class_name, $stmt->name->name);
3734
            }
3735
3736
            return null;
3737
        }
3738
3739
        if ($stmt instanceof PhpParser\Node\Scalar\String_
3740
            || $stmt instanceof PhpParser\Node\Scalar\LNumber
3741
            || $stmt instanceof PhpParser\Node\Scalar\DNumber
3742
        ) {
3743
            return new UnresolvedConstant\ScalarValue($stmt->value);
3744
        }
3745
3746
        if ($stmt instanceof PhpParser\Node\Expr\Array_) {
3747
            $items = [];
3748
3749
            foreach ($stmt->items as $item) {
3750
                if ($item === null) {
3751
                    return null;
3752
                }
3753
3754
                if ($item->key) {
3755
                    $item_key_type = self::getUnresolvedClassConstExpr(
3756
                        $item->key,
3757
                        $aliases,
3758
                        $fq_classlike_name
3759
                    );
3760
3761
                    if (!$item_key_type) {
3762
                        return null;
3763
                    }
3764
                } else {
3765
                    $item_key_type = null;
3766
                }
3767
3768
                $item_value_type = self::getUnresolvedClassConstExpr(
3769
                    $item->value,
3770
                    $aliases,
3771
                    $fq_classlike_name
3772
                );
3773
3774
                if (!$item_value_type) {
3775
                    return null;
3776
                }
3777
3778
                $items[] = new UnresolvedConstant\KeyValuePair($item_key_type, $item_value_type);
3779
            }
3780
3781
            return new UnresolvedConstant\ArrayValue($items);
3782
        }
3783
3784
        return null;
3785
    }
3786
3787
    /**
3788
     * @param  PhpParser\Node\Expr\Include_ $stmt
3789
     *
3790
     * @return void
3791
     */
3792
    public function visitInclude(PhpParser\Node\Expr\Include_ $stmt)
3793
    {
3794
        $config = Config::getInstance();
3795
3796
        if (!$config->allow_includes) {
3797
            throw new FileIncludeException(
3798
                'File includes are not allowed per your Psalm config - check the allowFileIncludes flag.'
3799
            );
3800
        }
3801
3802
        if ($stmt->expr instanceof PhpParser\Node\Scalar\String_) {
3803
            $path_to_file = $stmt->expr->value;
3804
3805
            // attempts to resolve using get_include_path dirs
3806
            $include_path = IncludeAnalyzer::resolveIncludePath($path_to_file, dirname($this->file_path));
3807
            $path_to_file = $include_path ? $include_path : $path_to_file;
3808
3809
            if (DIRECTORY_SEPARATOR === '/') {
3810
                $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR;
3811
            } else {
3812
                $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file);
3813
            }
3814
3815
            if ($is_path_relative) {
3816
                $path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file;
3817
            }
3818
        } else {
3819
            $path_to_file = IncludeAnalyzer::getPathTo(
3820
                $stmt->expr,
3821
                null,
3822
                null,
3823
                $this->file_path,
3824
                $this->config
3825
            );
3826
        }
3827
3828
        if ($path_to_file) {
3829
            $path_to_file = IncludeAnalyzer::normalizeFilePath($path_to_file);
3830
3831
            if ($this->file_path === $path_to_file) {
3832
                return;
3833
            }
3834
3835
            if ($this->codebase->fileExists($path_to_file)) {
3836
                if ($this->scan_deep) {
3837
                    $this->codebase->scanner->addFileToDeepScan($path_to_file);
3838
                } else {
3839
                    $this->codebase->scanner->addFileToShallowScan($path_to_file);
3840
                }
3841
3842
                $this->file_storage->required_file_paths[strtolower($path_to_file)] = $path_to_file;
3843
3844
                return;
3845
            }
3846
        }
3847
    }
3848
3849
    /**
3850
     * @return string
3851
     */
3852
    public function getFilePath()
3853
    {
3854
        return $this->file_path;
3855
    }
3856
3857
    /**
3858
     * @return string
3859
     */
3860
    public function getFileName()
3861
    {
3862
        return $this->file_scanner->getFileName();
3863
    }
3864
3865
    /**
3866
     * @return string
3867
     */
3868
    public function getRootFilePath()
3869
    {
3870
        return $this->file_scanner->getRootFilePath();
3871
    }
3872
3873
    /**
3874
     * @return string
3875
     */
3876
    public function getRootFileName()
3877
    {
3878
        return $this->file_scanner->getRootFileName();
3879
    }
3880
3881
    /**
3882
     * @return Aliases
3883
     */
3884
    public function getAliases()
3885
    {
3886
        return $this->aliases;
3887
    }
3888
3889
    public function afterTraverse(array $nodes)
3890
    {
3891
        $this->file_storage->type_aliases = $this->type_aliases;
3892
    }
3893
}
3894