Test Setup Failed
Push — master ( 0034f2...cf67b9 )
by Matthew
04:46
created

ClassAnalyzer   F

Complexity

Total Complexity 333

Size/Duplication

Total Lines 2137
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 83

Importance

Changes 0
Metric Value
dl 0
loc 2137
rs 0.8
c 0
b 0
f 0
wmc 333
lcom 1
cbo 83

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
A getAnonymousClassName() 0 5 1
F analyze() 0 887 144
F addContextProperties() 0 140 23
F checkPropertyInitialization() 0 336 55
D analyzeTraitUse() 0 122 17
D checkForMissingPropertyType() 0 93 16
B addOrUpdatePropertyType() 0 46 6
F analyzeClassMethod() 0 172 26
D analyzeClassMethodReturnType() 0 141 17
F checkTemplateParams() 0 118 24

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
namespace Psalm\Internal\Analyzer;
3
4
use PhpParser;
5
use Psalm\Aliases;
6
use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector;
7
use Psalm\Internal\FileManipulation\PropertyDocblockManipulator;
8
use Psalm\Internal\Type\UnionTemplateHandler;
9
use Psalm\Codebase;
10
use Psalm\CodeLocation;
11
use Psalm\Config;
12
use Psalm\Context;
13
use Psalm\Issue\DeprecatedClass;
14
use Psalm\Issue\DeprecatedInterface;
15
use Psalm\Issue\DeprecatedTrait;
16
use Psalm\Issue\InaccessibleMethod;
17
use Psalm\Issue\InternalClass;
18
use Psalm\Issue\InvalidExtendClass;
19
use Psalm\Issue\InvalidTemplateParam;
20
use Psalm\Issue\MethodSignatureMismatch;
21
use Psalm\Issue\MissingConstructor;
22
use Psalm\Issue\MissingImmutableAnnotation;
23
use Psalm\Issue\MissingPropertyType;
24
use Psalm\Issue\MissingTemplateParam;
25
use Psalm\Issue\MutableDependency;
26
use Psalm\Issue\OverriddenPropertyAccess;
27
use Psalm\Issue\PropertyNotSetInConstructor;
28
use Psalm\Issue\ReservedWord;
29
use Psalm\Issue\TooManyTemplateParams;
30
use Psalm\Issue\UndefinedClass;
31
use Psalm\Issue\UndefinedInterface;
32
use Psalm\Issue\UndefinedTrait;
33
use Psalm\Issue\UnimplementedAbstractMethod;
34
use Psalm\Issue\UnimplementedInterfaceMethod;
35
use Psalm\IssueBuffer;
36
use Psalm\StatementsSource;
37
use Psalm\Storage\ClassLikeStorage;
38
use Psalm\Storage\FunctionLikeParameter;
39
use Psalm\Type;
40
use function preg_replace;
41
use function preg_match;
42
use function explode;
43
use function array_pop;
44
use function strtolower;
45
use function implode;
46
use function substr;
47
use function array_map;
48
use function array_shift;
49
use function str_replace;
50
use function count;
51
use function array_search;
52
use function array_keys;
53
54
/**
55
 * @internal
56
 */
57
class ClassAnalyzer extends ClassLikeAnalyzer
58
{
59
    /**
60
     * @var array<string, Type\Union>
61
     */
62
    public $inferred_property_types = [];
63
64
    /**
65
     * @param PhpParser\Node\Stmt\Class_    $class
66
     * @param SourceAnalyzer                $source
67
     * @param string|null                   $fq_class_name
68
     */
69
    public function __construct(PhpParser\Node\Stmt\Class_ $class, SourceAnalyzer $source, $fq_class_name)
70
    {
71
        if (!$fq_class_name) {
72
            $fq_class_name = self::getAnonymousClassName($class, $source->getFilePath());
73
        }
74
75
        parent::__construct($class, $source, $fq_class_name);
76
77
        if (!$this->class instanceof PhpParser\Node\Stmt\Class_) {
78
            throw new \InvalidArgumentException('Bad');
79
        }
80
81
        if ($this->class->extends) {
82
            $this->parent_fq_class_name = self::getFQCLNFromNameObject(
83
                $this->class->extends,
84
                $this->source->getAliases()
85
            );
86
        }
87
    }
88
89
    /**
90
     * @param  PhpParser\Node\Stmt\Class_ $class
91
     * @param  string                     $file_path
92
     *
93
     * @return string
94
     */
95
    public static function getAnonymousClassName(PhpParser\Node\Stmt\Class_ $class, $file_path)
96
    {
97
        return preg_replace('/[^A-Za-z0-9]/', '_', $file_path)
98
            . '_' . $class->getLine() . '_' . (int)$class->getAttribute('startFilePos');
99
    }
100
101
    /**
102
     * @param Context|null  $class_context
103
     * @param Context|null  $global_context
104
     *
105
     * @return null|false
106
     */
107
    public function analyze(
108
        Context $class_context = null,
109
        Context $global_context = null
110
    ) {
111
        $class = $this->class;
112
113
        if (!$class instanceof PhpParser\Node\Stmt\Class_) {
114
            throw new \LogicException('Something went badly wrong');
115
        }
116
117
        $fq_class_name = $class_context && $class_context->self ? $class_context->self : $this->fq_class_name;
118
119
        $storage = $this->storage;
120
121
        if ($storage->has_visitor_issues) {
122
            return;
123
        }
124
125
        if ($class->name
126
            && (preg_match(
127
                '/(^|\\\)(int|float|bool|string|void|null|false|true|object|mixed)$/i',
128
                $fq_class_name
129
            ) || strtolower($fq_class_name) === 'resource')
130
        ) {
131
            $class_name_parts = explode('\\', $fq_class_name);
132
            $class_name = array_pop($class_name_parts);
133
134
            if (IssueBuffer::accepts(
135
                new ReservedWord(
136
                    $class_name . ' is a reserved word',
137
                    new CodeLocation(
138
                        $this,
139
                        $class->name,
140
                        null,
141
                        true
142
                    ),
143
                    $class_name
144
                ),
145
                $storage->suppressed_issues + $this->getSuppressedIssues()
146
            )) {
147
                // fall through
148
            }
149
150
            return null;
151
        }
152
153
        $project_analyzer = $this->file_analyzer->project_analyzer;
154
        $codebase = $this->getCodebase();
155
156
        if ($codebase->alter_code && $class->name && $codebase->classes_to_move) {
157
            if (isset($codebase->classes_to_move[strtolower($this->fq_class_name)])) {
158
                $destination_class = $codebase->classes_to_move[strtolower($this->fq_class_name)];
159
160
                $source_class_parts = explode('\\', $this->fq_class_name);
161
                $destination_class_parts = explode('\\', $destination_class);
162
163
                array_pop($source_class_parts);
164
                array_pop($destination_class_parts);
165
166
                $source_ns = implode('\\', $source_class_parts);
167
                $destination_ns = implode('\\', $destination_class_parts);
168
169
                if (strtolower($source_ns) !== strtolower($destination_ns)) {
170
                    if ($storage->namespace_name_location) {
171
                        $bounds = $storage->namespace_name_location->getSelectionBounds();
172
173
                        $file_manipulations = [
174
                            new \Psalm\FileManipulation(
175
                                $bounds[0],
176
                                $bounds[1],
177
                                $destination_ns
178
                            )
179
                        ];
180
181
                        \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
182
                            $this->getFilePath(),
183
                            $file_manipulations
184
                        );
185
                    } elseif (!$source_ns) {
186
                        $first_statement_pos = $this->getFileAnalyzer()->getFirstStatementOffset();
187
188
                        if ($first_statement_pos === -1) {
189
                            $first_statement_pos = (int) $class->getAttribute('startFilePos');
190
                        }
191
192
                        $file_manipulations = [
193
                            new \Psalm\FileManipulation(
194
                                $first_statement_pos,
195
                                $first_statement_pos,
196
                                'namespace ' . $destination_ns . ';' . "\n\n",
197
                                true
198
                            )
199
                        ];
200
201
                        \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
202
                            $this->getFilePath(),
203
                            $file_manipulations
204
                        );
205
                    }
206
                }
207
            }
208
209
            $codebase->classlikes->handleClassLikeReferenceInMigration(
210
                $codebase,
211
                $this,
212
                $class->name,
213
                $this->fq_class_name,
214
                null
215
            );
216
        }
217
218
        foreach ($storage->docblock_issues as $docblock_issue) {
219
            IssueBuffer::add($docblock_issue);
220
        }
221
222
        $classlike_storage_provider = $codebase->classlike_storage_provider;
223
224
        $parent_fq_class_name = $this->parent_fq_class_name;
225
226
        if ($class->extends) {
227
            if (!$parent_fq_class_name) {
228
                throw new \UnexpectedValueException('Parent class should be filled in for ' . $fq_class_name);
229
            }
230
231
            $parent_reference_location = new CodeLocation($this, $class->extends);
232
233
            if (self::checkFullyQualifiedClassLikeName(
234
                $this->getSource(),
235
                $parent_fq_class_name,
236
                $parent_reference_location,
237
                null,
238
                null,
239
                $storage->suppressed_issues + $this->getSuppressedIssues(),
240
                false
241
            ) === false) {
242
                return false;
243
            }
244
245
            if ($codebase->alter_code && $codebase->classes_to_move) {
246
                $codebase->classlikes->handleClassLikeReferenceInMigration(
247
                    $codebase,
248
                    $this,
249
                    $class->extends,
250
                    $parent_fq_class_name,
251
                    null
252
                );
253
            }
254
255
            try {
256
                $parent_class_storage = $classlike_storage_provider->get($parent_fq_class_name);
257
258
                $code_location = new CodeLocation(
259
                    $this,
260
                    $class->extends,
261
                    $class_context ? $class_context->include_location : null,
262
                    true
263
                );
264
265
                if ($parent_class_storage->is_trait || $parent_class_storage->is_interface) {
266
                    if (IssueBuffer::accepts(
267
                        new UndefinedClass(
268
                            $parent_fq_class_name . ' is not a class',
269
                            $code_location,
270
                            $parent_fq_class_name . ' as class'
271
                        ),
272
                        $storage->suppressed_issues + $this->getSuppressedIssues()
273
                    )) {
274
                        // fall through
275
                    }
276
                }
277
278
                if ($parent_class_storage->final) {
279
                    if (IssueBuffer::accepts(
280
                        new InvalidExtendClass(
281
                            'Class ' . $fq_class_name  . ' may not inherit from final class ' . $parent_fq_class_name,
282
                            $code_location,
283
                            $fq_class_name
284
                        ),
285
                        $storage->suppressed_issues + $this->getSuppressedIssues()
286
                    )) {
287
                        // fall through
288
                    }
289
                }
290
291
                if ($parent_class_storage->deprecated) {
292
                    if (IssueBuffer::accepts(
293
                        new DeprecatedClass(
294
                            $parent_fq_class_name . ' is marked deprecated',
295
                            $code_location,
296
                            $parent_fq_class_name
297
                        ),
298
                        $storage->suppressed_issues + $this->getSuppressedIssues()
299
                    )) {
300
                        // fall through
301
                    }
302
                }
303
304
                if ($parent_class_storage->internal) {
305
                    $code_location = new CodeLocation(
306
                        $this,
307
                        $class->extends,
308
                        $class_context ? $class_context->include_location : null,
309
                        true
310
                    );
311
                    if (! NamespaceAnalyzer::nameSpaceRootsMatch($fq_class_name, $parent_fq_class_name)) {
312
                        if (IssueBuffer::accepts(
313
                            new InternalClass(
314
                                $parent_fq_class_name . ' is marked internal',
315
                                $code_location,
316
                                $parent_fq_class_name
317
                            ),
318
                            $storage->suppressed_issues + $this->getSuppressedIssues()
319
                        )) {
320
                            // fall through
321
                        }
322
                    }
323
                }
324
325
                if ($parent_class_storage->psalm_internal &&
326
                    ! NamespaceAnalyzer::isWithin($fq_class_name, $parent_class_storage->psalm_internal)
327
                ) {
328
                    if (IssueBuffer::accepts(
329
                        new InternalClass(
330
                            $parent_fq_class_name . ' is internal to ' . $parent_class_storage->psalm_internal,
331
                            $code_location,
332
                            $parent_fq_class_name
333
                        ),
334
                        $storage->suppressed_issues + $this->getSuppressedIssues()
335
                    )) {
336
                        // fall through
337
                    }
338
                }
339
340
                if ($parent_class_storage->external_mutation_free
341
                    && !$storage->external_mutation_free
342
                ) {
343
                    if (IssueBuffer::accepts(
344
                        new MissingImmutableAnnotation(
345
                            $parent_fq_class_name . ' is marked immutable, but '
346
                                . $fq_class_name . ' is not marked immutable',
347
                            $code_location
348
                        ),
349
                        $storage->suppressed_issues + $this->getSuppressedIssues()
350
                    )) {
351
                        // fall through
352
                    }
353
                }
354
355
                if ($storage->mutation_free
356
                    && !$parent_class_storage->mutation_free
357
                ) {
358
                    if (IssueBuffer::accepts(
359
                        new MutableDependency(
360
                            $fq_class_name . ' is marked immutable but ' . $parent_fq_class_name . ' is not',
361
                            $code_location
362
                        ),
363
                        $storage->suppressed_issues + $this->getSuppressedIssues()
364
                    )) {
365
                        // fall through
366
                    }
367
                }
368
369
                if ($codebase->store_node_types) {
370
                    $codebase->analyzer->addNodeReference(
371
                        $this->getFilePath(),
372
                        $class->extends,
373
                        $codebase->classlikes->classExists($parent_fq_class_name)
374
                            ? $parent_fq_class_name
375
                            : '*' . implode('\\', $class->extends->parts)
376
                    );
377
                }
378
379
                $code_location = new CodeLocation(
380
                    $this,
381
                    $class->name ?: $class,
382
                    $class_context ? $class_context->include_location : null,
383
                    true
384
                );
385
386
                if ($storage->template_type_extends_count !== null) {
387
                    $this->checkTemplateParams(
388
                        $codebase,
389
                        $storage,
390
                        $parent_class_storage,
391
                        $code_location,
392
                        $storage->template_type_extends_count
393
                    );
394
                }
395
            } catch (\InvalidArgumentException $e) {
396
                // do nothing
397
            }
398
        }
399
400
        foreach ($class->implements as $interface_name) {
401
            $fq_interface_name = self::getFQCLNFromNameObject(
402
                $interface_name,
403
                $this->source->getAliases()
404
            );
405
406
            $codebase->analyzer->addNodeReference(
407
                $this->getFilePath(),
408
                $interface_name,
409
                $codebase->classlikes->interfaceExists($fq_interface_name)
410
                    ? $fq_interface_name
411
                    : '*' . implode('\\', $interface_name->parts)
412
            );
413
414
            $interface_location = new CodeLocation($this, $interface_name);
415
416
            if (self::checkFullyQualifiedClassLikeName(
417
                $this,
418
                $fq_interface_name,
419
                $interface_location,
420
                null,
421
                null,
422
                $this->getSuppressedIssues(),
423
                false
424
            ) === false) {
425
                continue;
426
            }
427
428
            if ($codebase->store_node_types && $fq_class_name) {
429
                $bounds = $interface_location->getSelectionBounds();
430
431
                $codebase->analyzer->addOffsetReference(
432
                    $this->getFilePath(),
433
                    $bounds[0],
434
                    $bounds[1],
435
                    $fq_interface_name
436
                );
437
            }
438
439
            $codebase->classlikes->handleClassLikeReferenceInMigration(
440
                $codebase,
441
                $this,
442
                $interface_name,
443
                $fq_interface_name,
444
                null
445
            );
446
447
            $fq_interface_name_lc = strtolower($fq_interface_name);
448
449
            try {
450
                $interface_storage = $classlike_storage_provider->get($fq_interface_name_lc);
451
            } catch (\InvalidArgumentException $e) {
452
                continue;
453
            }
454
455
            $code_location = new CodeLocation(
456
                $this,
457
                $interface_name,
458
                $class_context ? $class_context->include_location : null,
459
                true
460
            );
461
462
            if (!$interface_storage->is_interface) {
463
                if (IssueBuffer::accepts(
464
                    new UndefinedInterface(
465
                        $fq_interface_name . ' is not an interface',
466
                        $code_location,
467
                        $fq_interface_name
468
                    ),
469
                    $storage->suppressed_issues + $this->getSuppressedIssues()
470
                )) {
471
                    // fall through
472
                }
473
            }
474
475
            if (isset($storage->template_type_implements_count[$fq_interface_name_lc])) {
476
                $expected_param_count = $storage->template_type_implements_count[$fq_interface_name_lc];
477
478
                $this->checkTemplateParams(
479
                    $codebase,
480
                    $storage,
481
                    $interface_storage,
482
                    $code_location,
483
                    $expected_param_count
484
                );
485
            }
486
        }
487
488
        if ($storage->template_types) {
489
            foreach ($storage->template_types as $param_name => $_) {
490
                $fq_classlike_name = Type::getFQCLNFromString(
491
                    $param_name,
492
                    $this->getAliases()
493
                );
494
495
                if ($codebase->classOrInterfaceExists($fq_classlike_name)) {
496
                    if (IssueBuffer::accepts(
497
                        new ReservedWord(
498
                            'Cannot use ' . $param_name . ' as template name since the class already exists',
499
                            new CodeLocation($this, $this->class),
500
                            'resource'
501
                        ),
502
                        $this->getSuppressedIssues()
503
                    )) {
504
                        // fall through
505
                    }
506
                }
507
            }
508
        }
509
510
        if ($storage->mixin && $storage->mixin_declaring_fqcln === $storage->name) {
511
            $union = new Type\Union([$storage->mixin]);
512
            $union->check(
513
                $this,
514
                new CodeLocation(
515
                    $this,
516
                    $class->name ?: $class,
517
                    null,
518
                    true
519
                ),
520
                $this->getSuppressedIssues()
521
            );
522
        }
523
524
        if ($storage->template_type_extends) {
525
            foreach ($storage->template_type_extends as $type_map) {
526
                foreach ($type_map as $atomic_type) {
527
                    $atomic_type->check(
528
                        $this,
529
                        new CodeLocation(
530
                            $this,
531
                            $class->name ?: $class,
532
                            null,
533
                            true
534
                        ),
535
                        $this->getSuppressedIssues()
536
                    );
537
                }
538
            }
539
        }
540
541
        if ($storage->invalid_dependencies) {
542
            return;
543
        }
544
545
        $class_interfaces = $storage->class_implements;
546
547
        foreach ($class_interfaces as $interface_name) {
548
            try {
549
                $interface_storage = $classlike_storage_provider->get($interface_name);
550
            } catch (\InvalidArgumentException $e) {
551
                continue;
552
            }
553
554
            $code_location = new CodeLocation(
555
                $this,
556
                $class->name ? $class->name : $class,
557
                $class_context ? $class_context->include_location : null,
558
                true
559
            );
560
561
            if ($interface_storage->deprecated) {
562
                if (IssueBuffer::accepts(
563
                    new DeprecatedInterface(
564
                        $interface_name . ' is marked deprecated',
565
                        $code_location,
566
                        $interface_name
567
                    ),
568
                    $storage->suppressed_issues + $this->getSuppressedIssues()
569
                )) {
570
                    // fall through
571
                }
572
            }
573
574
            if ($interface_storage->external_mutation_free
575
                && !$storage->external_mutation_free
576
            ) {
577
                if (IssueBuffer::accepts(
578
                    new MissingImmutableAnnotation(
579
                        $interface_name . ' is marked immutable, but '
580
                            . $fq_class_name . ' is not marked immutable',
581
                        $code_location
582
                    ),
583
                    $storage->suppressed_issues + $this->getSuppressedIssues()
584
                )) {
585
                    // fall through
586
                }
587
            }
588
589
            foreach ($interface_storage->methods as $interface_method_name_lc => $interface_method_storage) {
590
                if ($interface_method_storage->visibility === self::VISIBILITY_PUBLIC) {
591
                    $implementer_declaring_method_id = $codebase->methods->getDeclaringMethodId(
592
                        new \Psalm\Internal\MethodIdentifier(
593
                            $this->fq_class_name,
594
                            $interface_method_name_lc
595
                        )
596
                    );
597
598
                    $implementer_method_storage = null;
599
                    $implementer_classlike_storage = null;
600
601
                    if ($implementer_declaring_method_id) {
602
                        $implementer_fq_class_name = $implementer_declaring_method_id->fq_class_name;
603
                        $implementer_method_storage = $codebase->methods->getStorage(
604
                            $implementer_declaring_method_id
605
                        );
606
                        $implementer_classlike_storage = $classlike_storage_provider->get(
607
                            $implementer_fq_class_name
608
                        );
609
                    }
610
611
                    if (!$implementer_method_storage) {
612
                        if (IssueBuffer::accepts(
613
                            new UnimplementedInterfaceMethod(
614
                                'Method ' . $interface_method_name_lc . ' is not defined on class ' .
615
                                $storage->name,
616
                                $code_location
617
                            ),
618
                            $storage->suppressed_issues + $this->getSuppressedIssues()
619
                        )) {
620
                            return false;
621
                        }
622
623
                        return null;
624
                    }
625
626
                    $implementer_appearing_method_id = $codebase->methods->getAppearingMethodId(
627
                        new \Psalm\Internal\MethodIdentifier(
628
                            $this->fq_class_name,
629
                            $interface_method_name_lc
630
                        )
631
                    );
632
633
                    $implementer_visibility = $implementer_method_storage->visibility;
634
635
                    if ($implementer_appearing_method_id
636
                        && $implementer_appearing_method_id !== $implementer_declaring_method_id
637
                    ) {
638
                        $appearing_fq_class_name = $implementer_appearing_method_id->fq_class_name;
639
                        $appearing_method_name = $implementer_appearing_method_id->method_name;
640
641
                        $appearing_class_storage = $classlike_storage_provider->get(
642
                            $appearing_fq_class_name
643
                        );
644
645
                        if (isset($appearing_class_storage->trait_visibility_map[$appearing_method_name])) {
646
                            $implementer_visibility
647
                                = $appearing_class_storage->trait_visibility_map[$appearing_method_name];
648
                        }
649
                    }
650
651
                    if ($implementer_visibility !== self::VISIBILITY_PUBLIC) {
652
                        if (IssueBuffer::accepts(
653
                            new InaccessibleMethod(
654
                                'Interface-defined method ' . $implementer_method_storage->cased_name
655
                                    . ' must be public in ' . $storage->name,
656
                                $code_location
657
                            ),
658
                            $storage->suppressed_issues + $this->getSuppressedIssues()
659
                        )) {
660
                            return false;
661
                        }
662
663
                        return null;
664
                    }
665
666
                    if ($interface_method_storage->is_static && !$implementer_method_storage->is_static) {
667
                        if (IssueBuffer::accepts(
668
                            new MethodSignatureMismatch(
669
                                'Method ' . $implementer_method_storage->cased_name
670
                                . ' should be static like '
671
                                . $storage->name . '::' . $interface_method_storage->cased_name,
672
                                $code_location
673
                            ),
674
                            $implementer_method_storage->suppressed_issues
675
                        )) {
676
                            return false;
677
                        }
678
                    }
679
680
                    if ($storage->abstract && $implementer_method_storage === $interface_method_storage) {
681
                        continue;
682
                    }
683
684
                    MethodComparator::compare(
685
                        $codebase,
686
                        $implementer_classlike_storage ?: $storage,
687
                        $interface_storage,
688
                        $implementer_method_storage,
689
                        $interface_method_storage,
690
                        $this->fq_class_name,
691
                        $implementer_visibility,
692
                        $code_location,
693
                        $implementer_method_storage->suppressed_issues,
694
                        false
695
                    );
696
                }
697
            }
698
        }
699
700
        if (!$class_context) {
701
            $class_context = new Context($this->fq_class_name);
702
            $class_context->parent = $parent_fq_class_name;
703
        }
704
705
        if ($global_context) {
706
            $class_context->strict_types = $global_context->strict_types;
707
        }
708
709
        if ($this->leftover_stmts) {
710
            (new StatementsAnalyzer(
711
                $this,
712
                new \Psalm\Internal\Provider\NodeDataProvider()
713
            ))->analyze(
714
                $this->leftover_stmts,
715
                $class_context
716
            );
717
        }
718
719
        if (!$storage->abstract) {
720
            foreach ($storage->declaring_method_ids as $declaring_method_id) {
721
                $method_storage = $codebase->methods->getStorage($declaring_method_id);
722
723
                $declaring_class_name = $declaring_method_id->fq_class_name;
724
                $method_name_lc = $declaring_method_id->method_name;
725
726
                if ($method_storage->abstract) {
727
                    if (IssueBuffer::accepts(
728
                        new UnimplementedAbstractMethod(
729
                            'Method ' . $method_name_lc . ' is not defined on class ' .
730
                            $this->fq_class_name . ', defined abstract in ' . $declaring_class_name,
731
                            new CodeLocation(
732
                                $this,
733
                                $class->name ? $class->name : $class,
734
                                $class_context->include_location,
735
                                true
736
                            )
737
                        ),
738
                        $storage->suppressed_issues + $this->getSuppressedIssues()
739
                    )) {
740
                        return false;
741
                    }
742
                }
743
            }
744
        }
745
746
        self::addContextProperties(
747
            $this,
748
            $storage,
749
            $class_context,
750
            $this->fq_class_name,
751
            $this->parent_fq_class_name
752
        );
753
754
        $constructor_analyzer = null;
755
        $member_stmts = [];
756
757
        foreach ($class->stmts as $stmt) {
758
            if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
759
                $method_analyzer = $this->analyzeClassMethod(
760
                    $stmt,
761
                    $storage,
762
                    $this,
763
                    $class_context,
764
                    $global_context
765
                );
766
767
                if ($stmt->name->name === '__construct') {
768
                    $constructor_analyzer = $method_analyzer;
769
                }
770
            } elseif ($stmt instanceof PhpParser\Node\Stmt\TraitUse) {
771
                if ($this->analyzeTraitUse(
772
                    $this->source->getAliases(),
773
                    $stmt,
774
                    $project_analyzer,
775
                    $storage,
776
                    $class_context,
777
                    $global_context,
778
                    $constructor_analyzer
779
                ) === false) {
780
                    return false;
781
                }
782
            } elseif ($stmt instanceof PhpParser\Node\Stmt\Property) {
783
                foreach ($stmt->props as $prop) {
784
                    if ($prop->default) {
785
                        $member_stmts[] = $stmt;
786
                    }
787
788
                    if ($codebase->alter_code) {
789
                        $property_id = strtolower($this->fq_class_name) . '::$' . $prop->name;
790
791
                        $property_storage = $codebase->properties->getStorage($property_id);
792
793
                        if ($property_storage->type
794
                            && $property_storage->type_location
795
                            && $property_storage->type_location !== $property_storage->signature_type_location
796
                        ) {
797
                            $replace_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
798
                                $codebase,
799
                                $property_storage->type,
800
                                $this->getFQCLN(),
801
                                $this->getFQCLN(),
802
                                $this->getParentFQCLN()
803
                            );
804
805
                            $codebase->classlikes->handleDocblockTypeInMigration(
806
                                $codebase,
807
                                $this,
808
                                $replace_type,
809
                                $property_storage->type_location,
810
                                null
811
                            );
812
                        }
813
814
                        foreach ($codebase->properties_to_rename as $original_property_id => $new_property_name) {
815
                            if ($property_id === $original_property_id) {
816
                                $file_manipulations = [
817
                                    new \Psalm\FileManipulation(
818
                                        (int) $prop->name->getAttribute('startFilePos'),
819
                                        (int) $prop->name->getAttribute('endFilePos') + 1,
820
                                        '$' . $new_property_name
821
                                    )
822
                                ];
823
824
                                \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
825
                                    $this->getFilePath(),
826
                                    $file_manipulations
827
                                );
828
                            }
829
                        }
830
                    }
831
                }
832
            } elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
833
                $member_stmts[] = $stmt;
834
835
                foreach ($stmt->consts as $const) {
836
                    $const_id = strtolower($this->fq_class_name) . '::' . $const->name;
837
838
                    foreach ($codebase->class_constants_to_rename as $original_const_id => $new_const_name) {
839
                        if ($const_id === $original_const_id) {
840
                            $file_manipulations = [
841
                                new \Psalm\FileManipulation(
842
                                    (int) $const->name->getAttribute('startFilePos'),
843
                                    (int) $const->name->getAttribute('endFilePos') + 1,
844
                                    $new_const_name
845
                                )
846
                            ];
847
848
                            \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
849
                                $this->getFilePath(),
850
                                $file_manipulations
851
                            );
852
                        }
853
                    }
854
                }
855
            }
856
        }
857
858
        $statements_analyzer = new StatementsAnalyzer($this, new \Psalm\Internal\Provider\NodeDataProvider());
859
        $statements_analyzer->analyze($member_stmts, $class_context, $global_context, true);
860
861
        $config = Config::getInstance();
862
863
        $this->checkPropertyInitialization(
864
            $codebase,
865
            $config,
866
            $storage,
867
            $class_context,
868
            $global_context,
869
            $constructor_analyzer
870
        );
871
872
        foreach ($class->stmts as $stmt) {
873
            if ($stmt instanceof PhpParser\Node\Stmt\Property && !isset($stmt->type)) {
874
                $this->checkForMissingPropertyType($this, $stmt, $class_context);
875
            } elseif ($stmt instanceof PhpParser\Node\Stmt\TraitUse) {
876
                foreach ($stmt->traits as $trait) {
877
                    $fq_trait_name = self::getFQCLNFromNameObject(
878
                        $trait,
879
                        $this->source->getAliases()
880
                    );
881
882
                    try {
883
                        $trait_file_analyzer = $project_analyzer->getFileAnalyzerForClassLike($fq_trait_name);
884
                    } catch (\Exception $e) {
885
                        continue;
886
                    }
887
888
                    $trait_storage = $codebase->classlike_storage_provider->get($fq_trait_name);
889
                    $trait_node = $codebase->classlikes->getTraitNode($fq_trait_name);
890
                    $trait_aliases = $trait_storage->aliases;
891
892
                    if ($trait_aliases === null) {
893
                        continue;
894
                    }
895
896
                    $trait_analyzer = new TraitAnalyzer(
897
                        $trait_node,
898
                        $trait_file_analyzer,
899
                        $fq_trait_name,
900
                        $trait_aliases
901
                    );
902
903
                    $fq_trait_name_lc = strtolower($fq_trait_name);
904
905
                    if (isset($storage->template_type_uses_count[$fq_trait_name_lc])) {
906
                        $expected_param_count = $storage->template_type_uses_count[$fq_trait_name_lc];
907
908
                        $this->checkTemplateParams(
909
                            $codebase,
910
                            $storage,
911
                            $trait_storage,
912
                            new CodeLocation(
913
                                $this,
914
                                $trait
915
                            ),
916
                            $expected_param_count
917
                        );
918
                    }
919
920
                    foreach ($trait_node->stmts as $trait_stmt) {
921
                        if ($trait_stmt instanceof PhpParser\Node\Stmt\Property) {
922
                            $this->checkForMissingPropertyType($trait_analyzer, $trait_stmt, $class_context);
923
                        }
924
                    }
925
926
                    $trait_file_analyzer->clearSourceBeforeDestruction();
927
                }
928
            }
929
        }
930
931
        $pseudo_methods = $storage->pseudo_methods + $storage->pseudo_static_methods;
932
933
        foreach ($pseudo_methods as $pseudo_method_name => $pseudo_method_storage) {
934
            $pseudo_method_id = new \Psalm\Internal\MethodIdentifier(
935
                $this->fq_class_name,
936
                $pseudo_method_name
937
            );
938
939
            $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($pseudo_method_id);
940
941
            if ($overridden_method_ids
942
                && $pseudo_method_name !== '__construct'
943
                && $pseudo_method_storage->location
944
            ) {
945
                foreach ($overridden_method_ids as $overridden_method_id) {
946
                    $parent_method_storage = $codebase->methods->getStorage($overridden_method_id);
947
948
                    $overridden_fq_class_name = $overridden_method_id->fq_class_name;
949
950
                    $parent_storage = $classlike_storage_provider->get($overridden_fq_class_name);
951
952
                    MethodComparator::compare(
953
                        $codebase,
954
                        $storage,
955
                        $parent_storage,
956
                        $pseudo_method_storage,
957
                        $parent_method_storage,
958
                        $this->fq_class_name,
959
                        $pseudo_method_storage->visibility ?: 0,
960
                        $storage->location ?: $pseudo_method_storage->location,
961
                        $storage->suppressed_issues,
962
                        true,
963
                        false
964
                    );
965
                }
966
            }
967
        }
968
969
        $plugin_classes = $codebase->config->after_classlike_checks;
970
971
        if ($plugin_classes) {
972
            $file_manipulations = [];
973
974
            foreach ($plugin_classes as $plugin_fq_class_name) {
975
                if ($plugin_fq_class_name::afterStatementAnalysis(
976
                    $class,
977
                    $storage,
978
                    $this,
979
                    $codebase,
980
                    $file_manipulations
981
                ) === false) {
982
                    return false;
983
                }
984
            }
985
986
            if ($file_manipulations) {
987
                \Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
988
                    $this->getFilePath(),
989
                    $file_manipulations
990
                );
991
            }
992
        }
993
    }
994
995
    public static function addContextProperties(
996
        StatementsSource $statements_source,
997
        ClassLikeStorage $storage,
998
        Context $class_context,
999
        string $fq_class_name,
1000
        ?string $parent_fq_class_name
1001
    ) : void {
1002
        $codebase = $statements_source->getCodebase();
1003
1004
        foreach ($storage->appearing_property_ids as $property_name => $appearing_property_id) {
1005
            $property_class_name = $codebase->properties->getDeclaringClassForProperty(
1006
                $appearing_property_id,
1007
                true
1008
            );
1009
1010
            if ($property_class_name === null) {
1011
                continue;
1012
            }
1013
1014
            $property_class_storage = $codebase->classlike_storage_provider->get($property_class_name);
1015
1016
            $property_storage = $property_class_storage->properties[$property_name];
1017
1018
            if (isset($storage->overridden_property_ids[$property_name])) {
1019
                foreach ($storage->overridden_property_ids[$property_name] as $overridden_property_id) {
1020
                    list($guide_class_name) = explode('::$', $overridden_property_id);
1021
                    $guide_class_storage = $codebase->classlike_storage_provider->get($guide_class_name);
1022
                    $guide_property_storage = $guide_class_storage->properties[$property_name];
1023
1024
                    if ($property_storage->visibility > $guide_property_storage->visibility
1025
                        && $property_storage->location
1026
                    ) {
1027
                        if (IssueBuffer::accepts(
1028
                            new OverriddenPropertyAccess(
1029
                                'Property ' . $guide_class_storage->name . '::$' . $property_name
1030
                                    . ' has different access level than '
1031
                                    . $storage->name . '::$' . $property_name,
1032
                                $property_storage->location
1033
                            )
1034
                        )) {
1035
                            // fall through
1036
                        }
1037
1038
                        continue;
1039
                    }
1040
                }
1041
            }
1042
1043
            if ($property_storage->type) {
1044
                $property_type = clone $property_storage->type;
1045
1046
                if (!$property_type->isMixed()
1047
                    && !$property_storage->has_default
1048
                    && !($property_type->isNullable() && $property_type->from_docblock)
1049
                ) {
1050
                    $property_type->initialized = false;
1051
                }
1052
            } else {
1053
                $property_type = Type::getMixed();
1054
1055
                if (!$property_storage->has_default) {
1056
                    $property_type->initialized = false;
1057
                }
1058
            }
1059
1060
            $property_type_location = $property_storage->type_location;
1061
1062
            $fleshed_out_type = !$property_type->isMixed()
1063
                ? \Psalm\Internal\Type\TypeExpander::expandUnion(
1064
                    $codebase,
1065
                    $property_type,
1066
                    $fq_class_name,
1067
                    $fq_class_name,
1068
                    $parent_fq_class_name
1069
                )
1070
                : $property_type;
1071
1072
            $class_template_params = ClassTemplateParamCollector::collect(
1073
                $codebase,
1074
                $property_class_storage,
1075
                $codebase->classlike_storage_provider->get($fq_class_name),
1076
                null,
1077
                new Type\Atomic\TNamedObject($fq_class_name),
1078
                '$this'
1079
            );
1080
1081
            $template_result = new \Psalm\Internal\Type\TemplateResult(
1082
                $class_template_params ?: [],
1083
                []
1084
            );
1085
1086
            if ($class_template_params) {
1087
                $fleshed_out_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
1088
                    $fleshed_out_type,
1089
                    $template_result,
1090
                    $codebase,
1091
                    null,
1092
                    null,
1093
                    null,
1094
                    $class_context->self
1095
                );
1096
            }
1097
1098
            if ($property_type_location && !$fleshed_out_type->isMixed()) {
1099
                $fleshed_out_type->check(
1100
                    $statements_source,
1101
                    $property_type_location,
1102
                    $storage->suppressed_issues + $statements_source->getSuppressedIssues(),
1103
                    [],
1104
                    false
1105
                );
1106
            }
1107
1108
            if ($property_storage->is_static) {
1109
                $property_id = $fq_class_name . '::$' . $property_name;
1110
1111
                $class_context->vars_in_scope[$property_id] = $fleshed_out_type;
1112
            } else {
1113
                $class_context->vars_in_scope['$this->' . $property_name] = $fleshed_out_type;
1114
            }
1115
        }
1116
1117
        foreach ($storage->pseudo_property_get_types as $property_name => $property_type) {
1118
            $property_name = substr($property_name, 1);
1119
1120
            if (isset($class_context->vars_in_scope['$this->' . $property_name])) {
1121
                $fleshed_out_type = !$property_type->isMixed()
1122
                    ? \Psalm\Internal\Type\TypeExpander::expandUnion(
1123
                        $codebase,
1124
                        $property_type,
1125
                        $fq_class_name,
1126
                        $fq_class_name,
1127
                        $parent_fq_class_name
1128
                    )
1129
                    : $property_type;
1130
1131
                $class_context->vars_in_scope['$this->' . $property_name] = $fleshed_out_type;
1132
            }
1133
        }
1134
    }
1135
1136
    /**
1137
     * @return void
1138
     */
1139
    private function checkPropertyInitialization(
1140
        Codebase $codebase,
1141
        Config $config,
1142
        ClassLikeStorage $storage,
1143
        Context $class_context,
1144
        Context $global_context = null,
1145
        MethodAnalyzer $constructor_analyzer = null
1146
    ) {
1147
        if (!$config->reportIssueInFile('PropertyNotSetInConstructor', $this->getFilePath())) {
1148
            return;
1149
        }
1150
1151
        if (!isset($storage->declaring_method_ids['__construct'])
1152
            && !$config->reportIssueInFile('MissingConstructor', $this->getFilePath())
1153
        ) {
1154
            return;
1155
        }
1156
1157
        $fq_class_name = $class_context->self ? $class_context->self : $this->fq_class_name;
1158
        $fq_class_name_lc = strtolower($fq_class_name);
1159
1160
        $included_file_path = $this->getFilePath();
1161
1162
        $method_already_analyzed = $codebase->analyzer->isMethodAlreadyAnalyzed(
1163
            $included_file_path,
1164
            $fq_class_name_lc . '::__construct',
1165
            true
1166
        );
1167
1168
        if ($method_already_analyzed && !$codebase->diff_methods) {
1169
            // this can happen when re-analysing a class that has been include()d inside another
1170
            return;
1171
        }
1172
1173
        /** @var PhpParser\Node\Stmt\Class_ */
1174
        $class = $this->class;
1175
        $classlike_storage_provider = $codebase->classlike_storage_provider;
1176
        $class_storage = $classlike_storage_provider->get($fq_class_name_lc);
1177
1178
        $constructor_appearing_fqcln = $fq_class_name_lc;
1179
1180
        $uninitialized_variables = [];
1181
        $uninitialized_properties = [];
1182
        $uninitialized_typed_properties = [];
1183
        $uninitialized_private_properties = false;
1184
1185
        foreach ($storage->appearing_property_ids as $property_name => $appearing_property_id) {
1186
            $property_class_name = $codebase->properties->getDeclaringClassForProperty(
1187
                $appearing_property_id,
1188
                true
1189
            );
1190
1191
            if ($property_class_name === null) {
1192
                continue;
1193
            }
1194
1195
            $property_class_storage = $classlike_storage_provider->get($property_class_name);
1196
1197
            $property = $property_class_storage->properties[$property_name];
1198
1199
            $property_is_initialized = isset($property_class_storage->initialized_properties[$property_name]);
1200
1201
            if ($property->is_static) {
1202
                continue;
1203
            }
1204
1205
            if ($property->has_default || $property_is_initialized) {
1206
                continue;
1207
            }
1208
1209
            if ($property->type && $property->type->isNullable() && $property->type->from_docblock) {
1210
                continue;
1211
            }
1212
1213
            if ($codebase->diff_methods && $method_already_analyzed && $property->location) {
1214
                list($start, $end) = $property->location->getSelectionBounds();
1215
1216
                $existing_issues = $codebase->analyzer->getExistingIssuesForFile(
1217
                    $this->getFilePath(),
1218
                    $start,
1219
                    $end,
1220
                    'PropertyNotSetInConstructor'
1221
                );
1222
1223
                if ($existing_issues) {
1224
                    IssueBuffer::addIssues([$this->getFilePath() => $existing_issues]);
1225
                    continue;
1226
                }
1227
            }
1228
1229
            if ($property->location) {
1230
                $codebase->analyzer->removeExistingDataForFile(
1231
                    $this->getFilePath(),
1232
                    $property->location->raw_file_start,
1233
                    $property->location->raw_file_end,
1234
                    'PropertyNotSetInConstructor'
1235
                );
1236
            }
1237
1238
            $codebase->file_reference_provider->addMethodReferenceToMissingClassMember(
1239
                $fq_class_name_lc . '::__construct',
1240
                strtolower($property_class_name) . '::$' . $property_name
1241
            );
1242
1243
            if ($property->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE) {
1244
                $uninitialized_private_properties = true;
1245
            }
1246
1247
            $uninitialized_variables[] = '$this->' . $property_name;
1248
            $uninitialized_properties[$property_class_name . '::$' . $property_name] = $property;
1249
1250
            if ($property->type && !$property->type->isMixed()) {
1251
                $uninitialized_typed_properties[$property_class_name . '::$' . $property_name] = $property;
1252
            }
1253
        }
1254
1255
        if (!$uninitialized_properties) {
1256
            return;
1257
        }
1258
1259
        if (!$storage->abstract
1260
            && !$constructor_analyzer
1261
            && isset($storage->declaring_method_ids['__construct'])
1262
            && isset($storage->appearing_method_ids['__construct'])
1263
            && $class->extends
1264
        ) {
1265
            $constructor_declaring_fqcln = $storage->declaring_method_ids['__construct']->fq_class_name;
1266
            $constructor_appearing_fqcln = $storage->appearing_method_ids['__construct']->fq_class_name;
1267
1268
            $constructor_class_storage = $classlike_storage_provider->get($constructor_declaring_fqcln);
1269
1270
            // ignore oldstyle constructors and classes without any declared properties
1271
            if ($constructor_class_storage->user_defined
1272
                && !$constructor_class_storage->stubbed
1273
                && isset($constructor_class_storage->methods['__construct'])
1274
            ) {
1275
                $constructor_storage = $constructor_class_storage->methods['__construct'];
1276
1277
                $fake_constructor_params = array_map(
1278
                    function (FunctionLikeParameter $param) : PhpParser\Node\Param {
1279
                        $fake_param = (new PhpParser\Builder\Param($param->name));
1280
                        if ($param->signature_type) {
1281
                            /** @psalm-suppress DeprecatedMethod */
1282
                            $fake_param->setTypeHint((string)$param->signature_type);
0 ignored issues
show
Deprecated Code introduced by
The method PhpParser\Builder\Param::setTypeHint() has been deprecated with message: Use setType() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1283
                        }
1284
1285
                        return $fake_param->getNode();
1286
                    },
1287
                    $constructor_storage->params
1288
                );
1289
1290
                $fake_constructor_stmt_args = array_map(
1291
                    function (FunctionLikeParameter $param) : PhpParser\Node\Arg {
1292
                        return new PhpParser\Node\Arg(new PhpParser\Node\Expr\Variable($param->name));
1293
                    },
1294
                    $constructor_storage->params
1295
                );
1296
1297
                $fake_constructor_stmts = [
1298
                    new PhpParser\Node\Stmt\Expression(
1299
                        new PhpParser\Node\Expr\StaticCall(
1300
                            new PhpParser\Node\Name\FullyQualified($constructor_declaring_fqcln),
1301
                            new PhpParser\Node\Identifier('__construct'),
1302
                            $fake_constructor_stmt_args,
1303
                            [
1304
                                'startLine' => $class->extends->getLine(),
1305
                                'startFilePos' => $class->extends->getAttribute('startFilePos'),
1306
                                'endFilePos' => $class->extends->getAttribute('endFilePos'),
1307
                                'comments' => [new PhpParser\Comment\Doc(
1308
                                    '/** @psalm-suppress InaccessibleMethod */',
1309
                                    $class->extends->getLine(),
1310
                                    (int) $class->extends->getAttribute('startFilePos')
1311
                                )],
1312
                            ]
1313
                        ),
1314
                        [
1315
                            'startLine' => $class->extends->getLine(),
1316
                            'startFilePos' => $class->extends->getAttribute('startFilePos'),
1317
                            'endFilePos' => $class->extends->getAttribute('endFilePos'),
1318
                            'comments' => [new PhpParser\Comment\Doc(
1319
                                '/** @psalm-suppress InaccessibleMethod */',
1320
                                $class->extends->getLine(),
1321
                                (int) $class->extends->getAttribute('startFilePos')
1322
                            )],
1323
                        ]
1324
                    ),
1325
                ];
1326
1327
                $fake_stmt = new PhpParser\Node\Stmt\ClassMethod(
1328
                    new PhpParser\Node\Identifier('__construct'),
1329
                    [
1330
                        'type' => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC,
1331
                        'params' => $fake_constructor_params,
1332
                        'stmts' => $fake_constructor_stmts,
1333
                    ],
1334
                    [
1335
                        'startLine' => $class->extends->getLine(),
1336
                        'startFilePos' => $class->extends->getAttribute('startFilePos'),
1337
                        'endFilePos' => $class->extends->getAttribute('endFilePos'),
1338
                    ]
1339
                );
1340
1341
                $codebase->analyzer->disableMixedCounts();
1342
1343
                $was_collecting_initializations = $class_context->collect_initializations;
1344
1345
                $class_context->collect_initializations = true;
1346
                $class_context->collect_nonprivate_initializations = !$uninitialized_private_properties;
1347
1348
                $constructor_analyzer = $this->analyzeClassMethod(
1349
                    $fake_stmt,
1350
                    $storage,
1351
                    $this,
1352
                    $class_context,
1353
                    $global_context,
1354
                    true
1355
                );
1356
1357
                $class_context->collect_initializations = $was_collecting_initializations;
1358
1359
                $codebase->analyzer->enableMixedCounts();
1360
            }
1361
        }
1362
1363
        if ($constructor_analyzer) {
1364
            $method_context = clone $class_context;
1365
            $method_context->collect_initializations = true;
1366
            $method_context->collect_nonprivate_initializations = !$uninitialized_private_properties;
1367
            $method_context->self = $fq_class_name;
1368
1369
            $this_atomic_object_type = new Type\Atomic\TNamedObject($fq_class_name);
1370
            $this_atomic_object_type->was_static = true;
1371
1372
            $method_context->vars_in_scope['$this'] = new Type\Union([$this_atomic_object_type]);
1373
            $method_context->vars_possibly_in_scope['$this'] = true;
1374
            $method_context->calling_method_id = strtolower($fq_class_name) . '::__construct';
1375
1376
            $constructor_analyzer->analyze(
1377
                $method_context,
1378
                new \Psalm\Internal\Provider\NodeDataProvider(),
1379
                $global_context,
1380
                true
1381
            );
1382
1383
            foreach ($uninitialized_properties as $property_id => $property_storage) {
1384
                list(,$property_name) = explode('::$', $property_id);
1385
1386
                if (!isset($method_context->vars_in_scope['$this->' . $property_name])) {
1387
                    $end_type = Type::getVoid();
1388
                    $end_type->initialized = false;
1389
                } else {
1390
                    $end_type = $method_context->vars_in_scope['$this->' . $property_name];
1391
                }
1392
1393
                $constructor_class_property_storage = $property_storage;
1394
1395
                $error_location = $property_storage->location;
1396
1397
                if ($storage->declaring_property_ids[$property_name] !== $fq_class_name) {
1398
                    $error_location = $storage->location ?: $storage->stmt_location;
1399
                }
1400
1401
                if ($fq_class_name_lc !== $constructor_appearing_fqcln
1402
                    && $property_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
1403
                ) {
1404
                    $a_class_storage = $classlike_storage_provider->get(
1405
                        $end_type->initialized_class ?: $constructor_appearing_fqcln
1406
                    );
1407
1408
                    if (!isset($a_class_storage->declaring_property_ids[$property_name])) {
1409
                        $constructor_class_property_storage = null;
1410
                    } else {
1411
                        $declaring_property_class = $a_class_storage->declaring_property_ids[$property_name];
1412
                        $constructor_class_property_storage = $classlike_storage_provider
1413
                            ->get($declaring_property_class)
1414
                            ->properties[$property_name];
1415
                    }
1416
                }
1417
1418
                if ($property_storage->location
1419
                    && $error_location
1420
                    && (!$end_type->initialized || $property_storage !== $constructor_class_property_storage)
1421
                ) {
1422
                    if ($property_storage->type) {
1423
                        $expected_visibility = $uninitialized_private_properties
1424
                            ? 'private or final '
1425
                            : '';
1426
1427
                        if (IssueBuffer::accepts(
1428
                            new PropertyNotSetInConstructor(
1429
                                'Property ' . $class_storage->name . '::$' . $property_name
1430
                                    . ' is not defined in constructor of '
1431
                                    . $this->fq_class_name . ' and in any ' . $expected_visibility
1432
                                    . 'methods called in the constructor',
1433
                                $error_location,
1434
                                $property_id
1435
                            ),
1436
                            $storage->suppressed_issues + $this->getSuppressedIssues()
1437
                        )) {
1438
                            // do nothing
1439
                        }
1440
                    } elseif (!$property_storage->has_default) {
1441
                        if (isset($this->inferred_property_types[$property_name])) {
1442
                            $this->inferred_property_types[$property_name]->addType(new Type\Atomic\TNull());
1443
                            $this->inferred_property_types[$property_name]->setFromDocblock();
1444
                        }
1445
                    }
1446
                }
1447
            }
1448
1449
            $codebase->analyzer->setAnalyzedMethod(
1450
                $included_file_path,
1451
                $fq_class_name_lc . '::__construct',
1452
                true
1453
            );
1454
1455
            return;
1456
        }
1457
1458
        if (!$storage->abstract && $uninitialized_typed_properties) {
1459
            $first_uninitialized_property = array_shift($uninitialized_typed_properties);
1460
1461
            if ($first_uninitialized_property->location) {
1462
                if (IssueBuffer::accepts(
1463
                    new MissingConstructor(
1464
                        $class_storage->name . ' has an uninitialized variable ' . $uninitialized_variables[0] .
1465
                            ', but no constructor',
1466
                        $first_uninitialized_property->location
1467
                    ),
1468
                    $storage->suppressed_issues + $this->getSuppressedIssues()
1469
                )) {
1470
                    // fall through
1471
                }
1472
            }
1473
        }
1474
    }
1475
1476
    /**
1477
     * @return false|null
1478
     */
1479
    private function analyzeTraitUse(
1480
        Aliases $aliases,
1481
        PhpParser\Node\Stmt\TraitUse $stmt,
1482
        ProjectAnalyzer $project_analyzer,
1483
        ClassLikeStorage $storage,
1484
        Context $class_context,
1485
        Context $global_context = null,
1486
        MethodAnalyzer &$constructor_analyzer = null
1487
    ) {
1488
        $codebase = $this->getCodebase();
1489
1490
        $previous_context_include_location = $class_context->include_location;
1491
1492
        foreach ($stmt->traits as $trait_name) {
1493
            $trait_location = new CodeLocation($this, $trait_name, null, true);
1494
            $class_context->include_location = new CodeLocation($this, $trait_name, null, true);
1495
1496
            $fq_trait_name = self::getFQCLNFromNameObject(
1497
                $trait_name,
1498
                $aliases
1499
            );
1500
1501
            if (!$codebase->classlikes->hasFullyQualifiedTraitName($fq_trait_name, $trait_location)) {
1502
                if (IssueBuffer::accepts(
1503
                    new UndefinedTrait(
1504
                        'Trait ' . $fq_trait_name . ' does not exist',
1505
                        new CodeLocation($this, $trait_name)
1506
                    ),
1507
                    $storage->suppressed_issues + $this->getSuppressedIssues()
1508
                )) {
1509
                    return false;
1510
                }
1511
            } else {
1512
                if (!$codebase->traitHasCorrectCase($fq_trait_name)) {
1513
                    if (IssueBuffer::accepts(
1514
                        new UndefinedTrait(
1515
                            'Trait ' . $fq_trait_name . ' has wrong casing',
1516
                            new CodeLocation($this, $trait_name)
1517
                        ),
1518
                        $storage->suppressed_issues + $this->getSuppressedIssues()
1519
                    )) {
1520
                        return false;
1521
                    }
1522
1523
                    continue;
1524
                }
1525
1526
                $fq_trait_name_resolved = $codebase->classlikes->getUnAliasedName($fq_trait_name);
1527
                $trait_storage = $codebase->classlike_storage_provider->get($fq_trait_name_resolved);
1528
1529
                if ($trait_storage->deprecated) {
1530
                    if (IssueBuffer::accepts(
1531
                        new DeprecatedTrait(
1532
                            'Trait ' . $fq_trait_name . ' is deprecated',
1533
                            new CodeLocation($this, $trait_name)
1534
                        ),
1535
                        $storage->suppressed_issues + $this->getSuppressedIssues()
1536
                    )) {
1537
                        // fall through
1538
                    }
1539
                }
1540
1541
                if ($storage->mutation_free && !$trait_storage->mutation_free) {
1542
                    if (IssueBuffer::accepts(
1543
                        new MutableDependency(
1544
                            $storage->name . ' is marked immutable but ' . $fq_trait_name . ' is not',
1545
                            new CodeLocation($this, $trait_name)
1546
                        ),
1547
                        $storage->suppressed_issues + $this->getSuppressedIssues()
1548
                    )) {
1549
                        // fall through
1550
                    }
1551
                }
1552
1553
                $trait_file_analyzer = $project_analyzer->getFileAnalyzerForClassLike($fq_trait_name_resolved);
1554
                $trait_node = $codebase->classlikes->getTraitNode($fq_trait_name_resolved);
1555
                $trait_aliases = $trait_storage->aliases;
1556
                if ($trait_aliases === null) {
1557
                    continue;
1558
                }
1559
1560
                $trait_analyzer = new TraitAnalyzer(
1561
                    $trait_node,
1562
                    $trait_file_analyzer,
1563
                    $fq_trait_name_resolved,
1564
                    $trait_aliases
1565
                );
1566
1567
                foreach ($trait_node->stmts as $trait_stmt) {
1568
                    if ($trait_stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
1569
                        $trait_method_analyzer = $this->analyzeClassMethod(
1570
                            $trait_stmt,
1571
                            $storage,
1572
                            $trait_analyzer,
1573
                            $class_context,
1574
                            $global_context
1575
                        );
1576
1577
                        if ($trait_stmt->name->name === '__construct') {
1578
                            $constructor_analyzer = $trait_method_analyzer;
1579
                        }
1580
                    } elseif ($trait_stmt instanceof PhpParser\Node\Stmt\TraitUse) {
1581
                        if ($this->analyzeTraitUse(
1582
                            $trait_aliases,
1583
                            $trait_stmt,
1584
                            $project_analyzer,
1585
                            $storage,
1586
                            $class_context,
1587
                            $global_context,
1588
                            $constructor_analyzer
1589
                        ) === false) {
1590
                            return false;
1591
                        }
1592
                    }
1593
                }
1594
1595
                $trait_file_analyzer->clearSourceBeforeDestruction();
1596
            }
1597
        }
1598
1599
        $class_context->include_location = $previous_context_include_location;
1600
    }
1601
1602
    /**
1603
     * @param   PhpParser\Node\Stmt\Property    $stmt
1604
     *
1605
     * @return  void
1606
     */
1607
    private function checkForMissingPropertyType(
1608
        StatementsSource $source,
1609
        PhpParser\Node\Stmt\Property $stmt,
1610
        Context $context
1611
    ) {
1612
        $comment = $stmt->getDocComment();
1613
1614
        if (!$comment || !$comment->getText()) {
1615
            $fq_class_name = $source->getFQCLN();
1616
            $property_name = $stmt->props[0]->name->name;
1617
1618
            $codebase = $this->getCodebase();
1619
1620
            $property_id = $fq_class_name . '::$' . $property_name;
1621
1622
            $declaring_property_class = $codebase->properties->getDeclaringClassForProperty(
1623
                $property_id,
1624
                true
1625
            );
1626
1627
            if (!$declaring_property_class) {
1628
                return;
1629
            }
1630
1631
            $fq_class_name = $declaring_property_class;
1632
1633
            // gets inherited property type
1634
            $class_property_type = $codebase->properties->getPropertyType($property_id, false, $source, $context);
1635
1636
            if ($class_property_type) {
1637
                return;
1638
            }
1639
1640
            $message = 'Property ' . $property_id . ' does not have a declared type';
1641
1642
            $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
1643
1644
            $property_storage = $class_storage->properties[$property_name];
1645
1646
            $suggested_type = $property_storage->suggested_type;
1647
1648
            if (isset($this->inferred_property_types[$property_name])) {
1649
                $suggested_type = $suggested_type
1650
                    ? Type::combineUnionTypes(
1651
                        $suggested_type,
1652
                        $this->inferred_property_types[$property_name],
1653
                        $codebase
1654
                    )
1655
                    : $this->inferred_property_types[$property_name];
1656
            }
1657
1658
            if ($suggested_type && !$suggested_type->isNull()) {
1659
                $message .= ' - consider ' . str_replace(
1660
                    ['<array-key, mixed>', '<empty, empty>'],
1661
                    '',
1662
                    (string)$suggested_type
1663
                );
1664
            }
1665
1666
            $project_analyzer = ProjectAnalyzer::getInstance();
1667
1668
            if ($codebase->alter_code
1669
                && isset($project_analyzer->getIssuesToFix()['MissingPropertyType'])
1670
                && !\in_array('MissingPropertyType', $this->getSuppressedIssues())
1671
                && $suggested_type
1672
            ) {
1673
                if ($suggested_type->hasMixed() || $suggested_type->isNull()) {
1674
                    return;
1675
                }
1676
1677
                self::addOrUpdatePropertyType(
1678
                    $project_analyzer,
1679
                    $stmt,
1680
                    $property_id,
1681
                    $suggested_type,
1682
                    $this,
1683
                    $suggested_type->from_docblock
1684
                );
1685
1686
                return;
1687
            }
1688
1689
            if (IssueBuffer::accepts(
1690
                new MissingPropertyType(
1691
                    $message,
1692
                    new CodeLocation($source, $stmt->props[0]->name)
1693
                ),
1694
                $this->source->getSuppressedIssues()
1695
            )) {
1696
                // fall through
1697
            }
1698
        }
1699
    }
1700
1701
    private static function addOrUpdatePropertyType(
1702
        ProjectAnalyzer $project_analyzer,
1703
        PhpParser\Node\Stmt\Property $property,
1704
        string $property_id,
1705
        Type\Union $inferred_type,
1706
        StatementsSource $source,
1707
        bool $docblock_only = false
1708
    ) : void {
1709
        $manipulator = PropertyDocblockManipulator::getForProperty(
1710
            $project_analyzer,
1711
            $source->getFilePath(),
1712
            $property_id,
1713
            $property
1714
        );
1715
1716
        $codebase = $project_analyzer->getCodebase();
1717
1718
        $allow_native_type = !$docblock_only
1719
            && $codebase->php_major_version >= 7
1720
            && ($codebase->php_major_version > 7 || $codebase->php_minor_version >= 4)
1721
            && $codebase->allow_backwards_incompatible_changes;
1722
1723
        $manipulator->setType(
1724
            $allow_native_type
1725
                ? (string) $inferred_type->toPhpString(
1726
                    $source->getNamespace(),
1727
                    $source->getAliasedClassesFlipped(),
1728
                    $source->getFQCLN(),
1729
                    $codebase->php_major_version,
1730
                    $codebase->php_minor_version
1731
                ) : null,
1732
            $inferred_type->toNamespacedString(
1733
                $source->getNamespace(),
1734
                $source->getAliasedClassesFlipped(),
1735
                $source->getFQCLN(),
1736
                false
1737
            ),
1738
            $inferred_type->toNamespacedString(
1739
                $source->getNamespace(),
1740
                $source->getAliasedClassesFlipped(),
1741
                $source->getFQCLN(),
1742
                true
1743
            ),
1744
            $inferred_type->canBeFullyExpressedInPhp()
1745
        );
1746
    }
1747
1748
    /**
1749
     * @param  PhpParser\Node\Stmt\ClassMethod $stmt
1750
     * @param  SourceAnalyzer                  $source
1751
     * @param  Context                         $class_context
1752
     * @param  Context|null                    $global_context
1753
     * @param  bool                            $is_fake
1754
     *
1755
     * @return MethodAnalyzer|null
1756
     */
1757
    private function analyzeClassMethod(
1758
        PhpParser\Node\Stmt\ClassMethod $stmt,
1759
        ClassLikeStorage $class_storage,
1760
        SourceAnalyzer $source,
1761
        Context $class_context,
1762
        Context $global_context = null,
1763
        $is_fake = false
1764
    ) {
1765
        $config = Config::getInstance();
1766
1767
        if ($stmt->stmts === null && !$stmt->isAbstract()) {
1768
            \Psalm\IssueBuffer::add(
1769
                new \Psalm\Issue\ParseError(
1770
                    'Non-abstract class method must have statements',
1771
                    new CodeLocation($this, $stmt)
1772
                )
1773
            );
1774
1775
            return null;
1776
        }
1777
1778
        try {
1779
            $method_analyzer = new MethodAnalyzer($stmt, $source);
1780
        } catch (\UnexpectedValueException $e) {
1781
            \Psalm\IssueBuffer::add(
1782
                new \Psalm\Issue\ParseError(
1783
                    'Problem loading method: ' . $e->getMessage(),
1784
                    new CodeLocation($this, $stmt)
1785
                )
1786
            );
1787
1788
            return null;
1789
        }
1790
1791
        $actual_method_id = $method_analyzer->getMethodId();
1792
1793
        $project_analyzer = $source->getProjectAnalyzer();
1794
        $codebase = $source->getCodebase();
1795
1796
        $analyzed_method_id = $actual_method_id;
1797
1798
        $included_file_path = $source->getFilePath();
1799
1800
        if ($class_context->self && strtolower($class_context->self) !== strtolower((string) $source->getFQCLN())) {
1801
            $analyzed_method_id = $method_analyzer->getMethodId($class_context->self);
1802
1803
            $declaring_method_id = $codebase->methods->getDeclaringMethodId($analyzed_method_id);
1804
1805
            if ((string) $actual_method_id !== (string) $declaring_method_id) {
1806
                // the method is an abstract trait method
1807
1808
                $declaring_method_storage = $method_analyzer->getFunctionLikeStorage();
1809
1810
                if (!$declaring_method_storage instanceof \Psalm\Storage\MethodStorage) {
1811
                    throw new \LogicException('This should never happen');
1812
                }
1813
1814
                if ($declaring_method_id && $declaring_method_storage->abstract) {
1815
                    $implementer_method_storage = $codebase->methods->getStorage($declaring_method_id);
1816
                    $declaring_storage = $codebase->classlike_storage_provider->get(
1817
                        $actual_method_id->fq_class_name
1818
                    );
1819
1820
                    MethodComparator::compare(
1821
                        $codebase,
1822
                        $class_storage,
1823
                        $declaring_storage,
1824
                        $implementer_method_storage,
1825
                        $declaring_method_storage,
1826
                        $this->fq_class_name,
1827
                        $implementer_method_storage->visibility,
1828
                        new CodeLocation($source, $stmt),
1829
                        $implementer_method_storage->suppressed_issues,
1830
                        false
1831
                    );
1832
                }
1833
1834
                return;
1835
            }
1836
        }
1837
1838
        $trait_safe_method_id = strtolower((string) $analyzed_method_id);
1839
1840
        $actual_method_id_str = strtolower((string) $actual_method_id);
1841
1842
        if ($actual_method_id_str !== $trait_safe_method_id) {
1843
            $trait_safe_method_id .= '&' . $actual_method_id_str;
1844
        }
1845
1846
        $method_already_analyzed = $codebase->analyzer->isMethodAlreadyAnalyzed(
1847
            $included_file_path,
1848
            $trait_safe_method_id
1849
        );
1850
1851
        $start = (int)$stmt->getAttribute('startFilePos');
1852
        $end = (int)$stmt->getAttribute('endFilePos');
1853
1854
        $comments = $stmt->getComments();
1855
1856
        if ($comments) {
1857
            $start = $comments[0]->getFilePos();
0 ignored issues
show
Deprecated Code introduced by
The method PhpParser\Comment::getFilePos() has been deprecated with message: Use getStartFilePos() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1858
        }
1859
1860
        if ($codebase->diff_methods
1861
            && $method_already_analyzed
1862
            && !$class_context->collect_initializations
1863
            && !$class_context->collect_mutations
1864
            && !$is_fake
1865
        ) {
1866
            $project_analyzer->progress->debug(
1867
                'Skipping analysis of pre-analyzed method ' . $analyzed_method_id . "\n"
1868
            );
1869
1870
            $existing_issues = $codebase->analyzer->getExistingIssuesForFile(
1871
                $source->getFilePath(),
1872
                $start,
1873
                $end
1874
            );
1875
1876
            IssueBuffer::addIssues([$source->getFilePath() => $existing_issues]);
1877
1878
            return $method_analyzer;
1879
        }
1880
1881
        $codebase->analyzer->removeExistingDataForFile(
1882
            $source->getFilePath(),
1883
            $start,
1884
            $end
1885
        );
1886
1887
        $method_context = clone $class_context;
1888
        foreach ($method_context->vars_in_scope as $context_var_id => $context_type) {
1889
            $method_context->vars_in_scope[$context_var_id] = clone $context_type;
1890
        }
1891
        $method_context->collect_exceptions = $config->check_for_throws_docblock;
1892
1893
        $type_provider = new \Psalm\Internal\Provider\NodeDataProvider();
1894
1895
        $method_analyzer->analyze(
1896
            $method_context,
1897
            $type_provider,
1898
            $global_context ? clone $global_context : null
1899
        );
1900
1901
        if ($stmt->name->name !== '__construct'
1902
            && $config->reportIssueInFile('InvalidReturnType', $source->getFilePath())
1903
            && $class_context->self
1904
        ) {
1905
            self::analyzeClassMethodReturnType(
1906
                $stmt,
1907
                $method_analyzer,
1908
                $source,
1909
                $type_provider,
1910
                $codebase,
1911
                $class_storage,
1912
                $class_context->self,
1913
                $analyzed_method_id,
1914
                $actual_method_id,
1915
                $method_context->has_returned
1916
            );
1917
        }
1918
1919
        if (!$method_already_analyzed
1920
            && !$class_context->collect_initializations
1921
            && !$class_context->collect_mutations
1922
            && !$is_fake
1923
        ) {
1924
            $codebase->analyzer->setAnalyzedMethod($included_file_path, $trait_safe_method_id);
1925
        }
1926
1927
        return $method_analyzer;
1928
    }
1929
1930
    public static function analyzeClassMethodReturnType(
1931
        PhpParser\Node\Stmt\ClassMethod $stmt,
1932
        MethodAnalyzer $method_analyzer,
1933
        SourceAnalyzer $source,
1934
        \Psalm\Internal\Provider\NodeDataProvider $type_provider,
1935
        Codebase $codebase,
1936
        ClassLikeStorage $class_storage,
1937
        string $fq_classlike_name,
1938
        \Psalm\Internal\MethodIdentifier $analyzed_method_id,
1939
        \Psalm\Internal\MethodIdentifier $actual_method_id,
1940
        bool $did_explicitly_return
1941
    ) : void {
1942
        $secondary_return_type_location = null;
1943
1944
        $actual_method_storage = $codebase->methods->getStorage($actual_method_id);
1945
1946
        $return_type_location = $codebase->methods->getMethodReturnTypeLocation(
1947
            $actual_method_id,
1948
            $secondary_return_type_location
1949
        );
1950
1951
        $original_fq_classlike_name = $fq_classlike_name;
1952
1953
        $return_type = $codebase->methods->getMethodReturnType(
1954
            $analyzed_method_id,
1955
            $fq_classlike_name,
1956
            $method_analyzer
1957
        );
1958
1959
        if ($return_type && $class_storage->template_type_extends) {
1960
            $declaring_method_id = $codebase->methods->getDeclaringMethodId($analyzed_method_id);
1961
1962
            if ($declaring_method_id) {
1963
                $declaring_class_name = $declaring_method_id->fq_class_name;
1964
1965
                $class_storage = $codebase->classlike_storage_provider->get($declaring_class_name);
1966
            }
1967
1968
            if ($class_storage->template_types) {
1969
                $template_params = [];
1970
1971
                foreach ($class_storage->template_types as $param_name => $template_map) {
1972
                    $key = array_keys($template_map)[0];
1973
1974
                    $template_params[] = new Type\Union([
1975
                        new Type\Atomic\TTemplateParam(
1976
                            $param_name,
1977
                            \reset($template_map)[0],
1978
                            $key
1979
                        )
1980
                    ]);
1981
                }
1982
1983
                $this_object_type = new Type\Atomic\TGenericObject(
1984
                    $original_fq_classlike_name,
1985
                    $template_params
1986
                );
1987
            } else {
1988
                $this_object_type = new Type\Atomic\TNamedObject($original_fq_classlike_name);
1989
            }
1990
1991
            $class_template_params = ClassTemplateParamCollector::collect(
1992
                $codebase,
1993
                $class_storage,
1994
                $codebase->classlike_storage_provider->get($original_fq_classlike_name),
1995
                strtolower($stmt->name->name),
1996
                $this_object_type
1997
            ) ?: [];
1998
1999
            $template_result = new \Psalm\Internal\Type\TemplateResult(
2000
                $class_template_params ?: [],
2001
                []
2002
            );
2003
2004
            $return_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
2005
                $return_type,
2006
                $template_result,
2007
                $codebase,
2008
                null,
2009
                null,
2010
                null,
2011
                $original_fq_classlike_name
2012
            );
2013
        }
2014
2015
        $overridden_method_ids = isset($class_storage->overridden_method_ids[strtolower($stmt->name->name)])
2016
            ? $class_storage->overridden_method_ids[strtolower($stmt->name->name)]
2017
            : [];
2018
2019
        if (!$return_type
2020
            && !$class_storage->is_interface
2021
            && $overridden_method_ids
2022
        ) {
2023
            foreach ($overridden_method_ids as $interface_method_id) {
2024
                $interface_class = $interface_method_id->fq_class_name;
2025
2026
                if (!$codebase->classlikes->interfaceExists($interface_class)) {
2027
                    continue;
2028
                }
2029
2030
                $interface_return_type = $codebase->methods->getMethodReturnType(
2031
                    $interface_method_id,
2032
                    $interface_class
2033
                );
2034
2035
                $interface_return_type_location = $codebase->methods->getMethodReturnTypeLocation(
2036
                    $interface_method_id
2037
                );
2038
2039
                FunctionLike\ReturnTypeAnalyzer::verifyReturnType(
2040
                    $stmt,
2041
                    $stmt->getStmts() ?: [],
2042
                    $source,
2043
                    $type_provider,
2044
                    $method_analyzer,
2045
                    $interface_return_type,
2046
                    $interface_class,
2047
                    $interface_return_type_location,
2048
                    [$analyzed_method_id],
2049
                    $did_explicitly_return
2050
                );
2051
            }
2052
        }
2053
2054
        if ($actual_method_storage->overridden_downstream) {
2055
            $overridden_method_ids['overridden::downstream'] = 'overridden::downstream';
2056
        }
2057
2058
        FunctionLike\ReturnTypeAnalyzer::verifyReturnType(
2059
            $stmt,
2060
            $stmt->getStmts() ?: [],
2061
            $source,
2062
            $type_provider,
2063
            $method_analyzer,
2064
            $return_type,
2065
            $fq_classlike_name,
2066
            $return_type_location,
2067
            $overridden_method_ids,
2068
            $did_explicitly_return
2069
        );
2070
    }
2071
2072
    /**
2073
     * @return void
2074
     */
2075
    private function checkTemplateParams(
2076
        Codebase $codebase,
2077
        ClassLikeStorage $storage,
2078
        ClassLikeStorage $parent_storage,
2079
        CodeLocation $code_location,
2080
        int $expected_param_count
2081
    ) {
2082
        $template_type_count = $parent_storage->template_types === null
2083
            ? 0
2084
            : count($parent_storage->template_types);
2085
2086
        if ($template_type_count > $expected_param_count) {
2087
            if (IssueBuffer::accepts(
2088
                new MissingTemplateParam(
2089
                    $storage->name . ' has missing template params, expecting '
2090
                        . $template_type_count,
2091
                    $code_location
2092
                ),
2093
                $storage->suppressed_issues + $this->getSuppressedIssues()
2094
            )) {
2095
                // fall through
2096
            }
2097
        } elseif ($template_type_count < $expected_param_count) {
2098
            if (IssueBuffer::accepts(
2099
                new TooManyTemplateParams(
2100
                    $storage->name . ' has too many template params, expecting '
2101
                        . $template_type_count,
2102
                    $code_location
2103
                ),
2104
                $storage->suppressed_issues + $this->getSuppressedIssues()
2105
            )) {
2106
                // fall through
2107
            }
2108
        }
2109
2110
        if ($parent_storage->template_types && $storage->template_type_extends) {
2111
            $i = 0;
2112
2113
            $previous_extended = [];
2114
2115
            foreach ($parent_storage->template_types as $template_name => $type_map) {
2116
                foreach ($type_map as $declaring_class => $template_type) {
2117
                    if (isset($storage->template_type_extends[$parent_storage->name][$template_name])) {
2118
                        $extended_type = $storage->template_type_extends[$parent_storage->name][$template_name];
2119
2120
                        if (isset($parent_storage->template_covariants[$i])
2121
                            && !$parent_storage->template_covariants[$i]
2122
                        ) {
2123
                            foreach ($extended_type->getAtomicTypes() as $t) {
2124
                                if ($t instanceof Type\Atomic\TTemplateParam
2125
                                    && $storage->template_types
2126
                                    && $storage->template_covariants
2127
                                    && ($local_offset
2128
                                        = array_search($t->param_name, array_keys($storage->template_types)))
2129
                                        !== false
2130
                                    && !empty($storage->template_covariants[$local_offset])
2131
                                ) {
2132
                                    if (IssueBuffer::accepts(
2133
                                        new InvalidTemplateParam(
2134
                                            'Cannot extend an invariant template param ' . $template_name
2135
                                                . ' into a covariant context',
2136
                                            $code_location
2137
                                        ),
2138
                                        $storage->suppressed_issues + $this->getSuppressedIssues()
2139
                                    )) {
2140
                                        // fall through
2141
                                    }
2142
                                }
2143
                            }
2144
                        }
2145
2146
                        if (!$template_type[0]->isMixed()) {
2147
                            $template_type_copy = clone $template_type[0];
2148
2149
                            $template_result = new \Psalm\Internal\Type\TemplateResult(
2150
                                $previous_extended ?: [],
2151
                                []
2152
                            );
2153
2154
                            $template_type_copy = UnionTemplateHandler::replaceTemplateTypesWithStandins(
2155
                                $template_type_copy,
2156
                                $template_result,
2157
                                $codebase,
2158
                                null,
2159
                                $extended_type,
2160
                                null,
2161
                                null
2162
                            );
2163
2164
                            if (!TypeAnalyzer::isContainedBy($codebase, $extended_type, $template_type_copy)) {
2165
                                if (IssueBuffer::accepts(
2166
                                    new InvalidTemplateParam(
2167
                                        'Extended template param ' . $template_name
2168
                                            . ' expects type ' . $template_type_copy->getId()
2169
                                            . ', type ' . $extended_type->getId() . ' given',
2170
                                        $code_location
2171
                                    ),
2172
                                    $storage->suppressed_issues + $this->getSuppressedIssues()
2173
                                )) {
2174
                                    // fall through
2175
                                }
2176
                            } else {
2177
                                $previous_extended[$template_name] = [
2178
                                    $declaring_class => [$extended_type]
2179
                                ];
2180
                            }
2181
                        } else {
2182
                            $previous_extended[$template_name] = [
2183
                                $declaring_class => [$extended_type]
2184
                            ];
2185
                        }
2186
                    }
2187
                }
2188
2189
                $i++;
2190
            }
2191
        }
2192
    }
2193
}
2194