ClassAnalyzer   F
last analyzed

Complexity

Total Complexity 339

Size/Duplication

Total Lines 2142
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 83

Importance

Changes 0
Metric Value
dl 0
loc 2142
rs 0.8
c 0
b 0
f 0
wmc 339
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 98 22
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
            $class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
1637
1638
            $property_storage = $class_storage->properties[$property_name];
1639
1640
            if ($class_property_type && ($property_storage->type_location || !$codebase->alter_code)) {
1641
                return;
1642
            }
1643
1644
            $message = 'Property ' . $property_id . ' does not have a declared type';
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 && !$property_storage->has_default && $property_storage->is_static) {
1659
                $suggested_type->addType(new Type\Atomic\TNull());
1660
            }
1661
1662
            if ($suggested_type && !$suggested_type->isNull()) {
1663
                $message .= ' - consider ' . str_replace(
1664
                    ['<array-key, mixed>', '<empty, empty>'],
1665
                    '',
1666
                    (string)$suggested_type
1667
                );
1668
            }
1669
1670
            $project_analyzer = ProjectAnalyzer::getInstance();
1671
1672
            if ($codebase->alter_code
1673
                && $source === $this
1674
                && isset($project_analyzer->getIssuesToFix()['MissingPropertyType'])
1675
                && !\in_array('MissingPropertyType', $this->getSuppressedIssues())
1676
                && $suggested_type
1677
            ) {
1678
                if ($suggested_type->hasMixed() || $suggested_type->isNull()) {
1679
                    return;
1680
                }
1681
1682
                self::addOrUpdatePropertyType(
1683
                    $project_analyzer,
1684
                    $stmt,
1685
                    $property_id,
1686
                    $suggested_type,
1687
                    $this,
1688
                    $suggested_type->from_docblock
1689
                );
1690
1691
                return;
1692
            }
1693
1694
            if (IssueBuffer::accepts(
1695
                new MissingPropertyType(
1696
                    $message,
1697
                    new CodeLocation($source, $stmt->props[0]->name)
1698
                ),
1699
                $this->source->getSuppressedIssues()
1700
            )) {
1701
                // fall through
1702
            }
1703
        }
1704
    }
1705
1706
    private static function addOrUpdatePropertyType(
1707
        ProjectAnalyzer $project_analyzer,
1708
        PhpParser\Node\Stmt\Property $property,
1709
        string $property_id,
1710
        Type\Union $inferred_type,
1711
        StatementsSource $source,
1712
        bool $docblock_only = false
1713
    ) : void {
1714
        $manipulator = PropertyDocblockManipulator::getForProperty(
1715
            $project_analyzer,
1716
            $source->getFilePath(),
1717
            $property_id,
1718
            $property
1719
        );
1720
1721
        $codebase = $project_analyzer->getCodebase();
1722
1723
        $allow_native_type = !$docblock_only
1724
            && $codebase->php_major_version >= 7
1725
            && ($codebase->php_major_version > 7 || $codebase->php_minor_version >= 4)
1726
            && $codebase->allow_backwards_incompatible_changes;
1727
1728
        $manipulator->setType(
1729
            $allow_native_type
1730
                ? (string) $inferred_type->toPhpString(
1731
                    $source->getNamespace(),
1732
                    $source->getAliasedClassesFlipped(),
1733
                    $source->getFQCLN(),
1734
                    $codebase->php_major_version,
1735
                    $codebase->php_minor_version
1736
                ) : null,
1737
            $inferred_type->toNamespacedString(
1738
                $source->getNamespace(),
1739
                $source->getAliasedClassesFlipped(),
1740
                $source->getFQCLN(),
1741
                false
1742
            ),
1743
            $inferred_type->toNamespacedString(
1744
                $source->getNamespace(),
1745
                $source->getAliasedClassesFlipped(),
1746
                $source->getFQCLN(),
1747
                true
1748
            ),
1749
            $inferred_type->canBeFullyExpressedInPhp()
1750
        );
1751
    }
1752
1753
    /**
1754
     * @param  PhpParser\Node\Stmt\ClassMethod $stmt
1755
     * @param  SourceAnalyzer                  $source
1756
     * @param  Context                         $class_context
1757
     * @param  Context|null                    $global_context
1758
     * @param  bool                            $is_fake
1759
     *
1760
     * @return MethodAnalyzer|null
1761
     */
1762
    private function analyzeClassMethod(
1763
        PhpParser\Node\Stmt\ClassMethod $stmt,
1764
        ClassLikeStorage $class_storage,
1765
        SourceAnalyzer $source,
1766
        Context $class_context,
1767
        Context $global_context = null,
1768
        $is_fake = false
1769
    ) {
1770
        $config = Config::getInstance();
1771
1772
        if ($stmt->stmts === null && !$stmt->isAbstract()) {
1773
            \Psalm\IssueBuffer::add(
1774
                new \Psalm\Issue\ParseError(
1775
                    'Non-abstract class method must have statements',
1776
                    new CodeLocation($this, $stmt)
1777
                )
1778
            );
1779
1780
            return null;
1781
        }
1782
1783
        try {
1784
            $method_analyzer = new MethodAnalyzer($stmt, $source);
1785
        } catch (\UnexpectedValueException $e) {
1786
            \Psalm\IssueBuffer::add(
1787
                new \Psalm\Issue\ParseError(
1788
                    'Problem loading method: ' . $e->getMessage(),
1789
                    new CodeLocation($this, $stmt)
1790
                )
1791
            );
1792
1793
            return null;
1794
        }
1795
1796
        $actual_method_id = $method_analyzer->getMethodId();
1797
1798
        $project_analyzer = $source->getProjectAnalyzer();
1799
        $codebase = $source->getCodebase();
1800
1801
        $analyzed_method_id = $actual_method_id;
1802
1803
        $included_file_path = $source->getFilePath();
1804
1805
        if ($class_context->self && strtolower($class_context->self) !== strtolower((string) $source->getFQCLN())) {
1806
            $analyzed_method_id = $method_analyzer->getMethodId($class_context->self);
1807
1808
            $declaring_method_id = $codebase->methods->getDeclaringMethodId($analyzed_method_id);
1809
1810
            if ((string) $actual_method_id !== (string) $declaring_method_id) {
1811
                // the method is an abstract trait method
1812
1813
                $declaring_method_storage = $method_analyzer->getFunctionLikeStorage();
1814
1815
                if (!$declaring_method_storage instanceof \Psalm\Storage\MethodStorage) {
1816
                    throw new \LogicException('This should never happen');
1817
                }
1818
1819
                if ($declaring_method_id && $declaring_method_storage->abstract) {
1820
                    $implementer_method_storage = $codebase->methods->getStorage($declaring_method_id);
1821
                    $declaring_storage = $codebase->classlike_storage_provider->get(
1822
                        $actual_method_id->fq_class_name
1823
                    );
1824
1825
                    MethodComparator::compare(
1826
                        $codebase,
1827
                        $class_storage,
1828
                        $declaring_storage,
1829
                        $implementer_method_storage,
1830
                        $declaring_method_storage,
1831
                        $this->fq_class_name,
1832
                        $implementer_method_storage->visibility,
1833
                        new CodeLocation($source, $stmt),
1834
                        $implementer_method_storage->suppressed_issues,
1835
                        false
1836
                    );
1837
                }
1838
1839
                return;
1840
            }
1841
        }
1842
1843
        $trait_safe_method_id = strtolower((string) $analyzed_method_id);
1844
1845
        $actual_method_id_str = strtolower((string) $actual_method_id);
1846
1847
        if ($actual_method_id_str !== $trait_safe_method_id) {
1848
            $trait_safe_method_id .= '&' . $actual_method_id_str;
1849
        }
1850
1851
        $method_already_analyzed = $codebase->analyzer->isMethodAlreadyAnalyzed(
1852
            $included_file_path,
1853
            $trait_safe_method_id
1854
        );
1855
1856
        $start = (int)$stmt->getAttribute('startFilePos');
1857
        $end = (int)$stmt->getAttribute('endFilePos');
1858
1859
        $comments = $stmt->getComments();
1860
1861
        if ($comments) {
1862
            $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...
1863
        }
1864
1865
        if ($codebase->diff_methods
1866
            && $method_already_analyzed
1867
            && !$class_context->collect_initializations
1868
            && !$class_context->collect_mutations
1869
            && !$is_fake
1870
        ) {
1871
            $project_analyzer->progress->debug(
1872
                'Skipping analysis of pre-analyzed method ' . $analyzed_method_id . "\n"
1873
            );
1874
1875
            $existing_issues = $codebase->analyzer->getExistingIssuesForFile(
1876
                $source->getFilePath(),
1877
                $start,
1878
                $end
1879
            );
1880
1881
            IssueBuffer::addIssues([$source->getFilePath() => $existing_issues]);
1882
1883
            return $method_analyzer;
1884
        }
1885
1886
        $codebase->analyzer->removeExistingDataForFile(
1887
            $source->getFilePath(),
1888
            $start,
1889
            $end
1890
        );
1891
1892
        $method_context = clone $class_context;
1893
        foreach ($method_context->vars_in_scope as $context_var_id => $context_type) {
1894
            $method_context->vars_in_scope[$context_var_id] = clone $context_type;
1895
        }
1896
        $method_context->collect_exceptions = $config->check_for_throws_docblock;
1897
1898
        $type_provider = new \Psalm\Internal\Provider\NodeDataProvider();
1899
1900
        $method_analyzer->analyze(
1901
            $method_context,
1902
            $type_provider,
1903
            $global_context ? clone $global_context : null
1904
        );
1905
1906
        if ($stmt->name->name !== '__construct'
1907
            && $config->reportIssueInFile('InvalidReturnType', $source->getFilePath())
1908
            && $class_context->self
1909
        ) {
1910
            self::analyzeClassMethodReturnType(
1911
                $stmt,
1912
                $method_analyzer,
1913
                $source,
1914
                $type_provider,
1915
                $codebase,
1916
                $class_storage,
1917
                $class_context->self,
1918
                $analyzed_method_id,
1919
                $actual_method_id,
1920
                $method_context->has_returned
1921
            );
1922
        }
1923
1924
        if (!$method_already_analyzed
1925
            && !$class_context->collect_initializations
1926
            && !$class_context->collect_mutations
1927
            && !$is_fake
1928
        ) {
1929
            $codebase->analyzer->setAnalyzedMethod($included_file_path, $trait_safe_method_id);
1930
        }
1931
1932
        return $method_analyzer;
1933
    }
1934
1935
    public static function analyzeClassMethodReturnType(
1936
        PhpParser\Node\Stmt\ClassMethod $stmt,
1937
        MethodAnalyzer $method_analyzer,
1938
        SourceAnalyzer $source,
1939
        \Psalm\Internal\Provider\NodeDataProvider $type_provider,
1940
        Codebase $codebase,
1941
        ClassLikeStorage $class_storage,
1942
        string $fq_classlike_name,
1943
        \Psalm\Internal\MethodIdentifier $analyzed_method_id,
1944
        \Psalm\Internal\MethodIdentifier $actual_method_id,
1945
        bool $did_explicitly_return
1946
    ) : void {
1947
        $secondary_return_type_location = null;
1948
1949
        $actual_method_storage = $codebase->methods->getStorage($actual_method_id);
1950
1951
        $return_type_location = $codebase->methods->getMethodReturnTypeLocation(
1952
            $actual_method_id,
1953
            $secondary_return_type_location
1954
        );
1955
1956
        $original_fq_classlike_name = $fq_classlike_name;
1957
1958
        $return_type = $codebase->methods->getMethodReturnType(
1959
            $analyzed_method_id,
1960
            $fq_classlike_name,
1961
            $method_analyzer
1962
        );
1963
1964
        if ($return_type && $class_storage->template_type_extends) {
1965
            $declaring_method_id = $codebase->methods->getDeclaringMethodId($analyzed_method_id);
1966
1967
            if ($declaring_method_id) {
1968
                $declaring_class_name = $declaring_method_id->fq_class_name;
1969
1970
                $class_storage = $codebase->classlike_storage_provider->get($declaring_class_name);
1971
            }
1972
1973
            if ($class_storage->template_types) {
1974
                $template_params = [];
1975
1976
                foreach ($class_storage->template_types as $param_name => $template_map) {
1977
                    $key = array_keys($template_map)[0];
1978
1979
                    $template_params[] = new Type\Union([
1980
                        new Type\Atomic\TTemplateParam(
1981
                            $param_name,
1982
                            \reset($template_map)[0],
1983
                            $key
1984
                        )
1985
                    ]);
1986
                }
1987
1988
                $this_object_type = new Type\Atomic\TGenericObject(
1989
                    $original_fq_classlike_name,
1990
                    $template_params
1991
                );
1992
            } else {
1993
                $this_object_type = new Type\Atomic\TNamedObject($original_fq_classlike_name);
1994
            }
1995
1996
            $class_template_params = ClassTemplateParamCollector::collect(
1997
                $codebase,
1998
                $class_storage,
1999
                $codebase->classlike_storage_provider->get($original_fq_classlike_name),
2000
                strtolower($stmt->name->name),
2001
                $this_object_type
2002
            ) ?: [];
2003
2004
            $template_result = new \Psalm\Internal\Type\TemplateResult(
2005
                $class_template_params ?: [],
2006
                []
2007
            );
2008
2009
            $return_type = UnionTemplateHandler::replaceTemplateTypesWithStandins(
2010
                $return_type,
2011
                $template_result,
2012
                $codebase,
2013
                null,
2014
                null,
2015
                null,
2016
                $original_fq_classlike_name
2017
            );
2018
        }
2019
2020
        $overridden_method_ids = isset($class_storage->overridden_method_ids[strtolower($stmt->name->name)])
2021
            ? $class_storage->overridden_method_ids[strtolower($stmt->name->name)]
2022
            : [];
2023
2024
        if (!$return_type
2025
            && !$class_storage->is_interface
2026
            && $overridden_method_ids
2027
        ) {
2028
            foreach ($overridden_method_ids as $interface_method_id) {
2029
                $interface_class = $interface_method_id->fq_class_name;
2030
2031
                if (!$codebase->classlikes->interfaceExists($interface_class)) {
2032
                    continue;
2033
                }
2034
2035
                $interface_return_type = $codebase->methods->getMethodReturnType(
2036
                    $interface_method_id,
2037
                    $interface_class
2038
                );
2039
2040
                $interface_return_type_location = $codebase->methods->getMethodReturnTypeLocation(
2041
                    $interface_method_id
2042
                );
2043
2044
                FunctionLike\ReturnTypeAnalyzer::verifyReturnType(
2045
                    $stmt,
2046
                    $stmt->getStmts() ?: [],
2047
                    $source,
2048
                    $type_provider,
2049
                    $method_analyzer,
2050
                    $interface_return_type,
2051
                    $interface_class,
2052
                    $interface_return_type_location,
2053
                    [$analyzed_method_id],
2054
                    $did_explicitly_return
2055
                );
2056
            }
2057
        }
2058
2059
        if ($actual_method_storage->overridden_downstream) {
2060
            $overridden_method_ids['overridden::downstream'] = 'overridden::downstream';
2061
        }
2062
2063
        FunctionLike\ReturnTypeAnalyzer::verifyReturnType(
2064
            $stmt,
2065
            $stmt->getStmts() ?: [],
2066
            $source,
2067
            $type_provider,
2068
            $method_analyzer,
2069
            $return_type,
2070
            $fq_classlike_name,
2071
            $return_type_location,
2072
            $overridden_method_ids,
2073
            $did_explicitly_return
2074
        );
2075
    }
2076
2077
    /**
2078
     * @return void
2079
     */
2080
    private function checkTemplateParams(
2081
        Codebase $codebase,
2082
        ClassLikeStorage $storage,
2083
        ClassLikeStorage $parent_storage,
2084
        CodeLocation $code_location,
2085
        int $expected_param_count
2086
    ) {
2087
        $template_type_count = $parent_storage->template_types === null
2088
            ? 0
2089
            : count($parent_storage->template_types);
2090
2091
        if ($template_type_count > $expected_param_count) {
2092
            if (IssueBuffer::accepts(
2093
                new MissingTemplateParam(
2094
                    $storage->name . ' has missing template params, expecting '
2095
                        . $template_type_count,
2096
                    $code_location
2097
                ),
2098
                $storage->suppressed_issues + $this->getSuppressedIssues()
2099
            )) {
2100
                // fall through
2101
            }
2102
        } elseif ($template_type_count < $expected_param_count) {
2103
            if (IssueBuffer::accepts(
2104
                new TooManyTemplateParams(
2105
                    $storage->name . ' has too many template params, expecting '
2106
                        . $template_type_count,
2107
                    $code_location
2108
                ),
2109
                $storage->suppressed_issues + $this->getSuppressedIssues()
2110
            )) {
2111
                // fall through
2112
            }
2113
        }
2114
2115
        if ($parent_storage->template_types && $storage->template_type_extends) {
2116
            $i = 0;
2117
2118
            $previous_extended = [];
2119
2120
            foreach ($parent_storage->template_types as $template_name => $type_map) {
2121
                foreach ($type_map as $declaring_class => $template_type) {
2122
                    if (isset($storage->template_type_extends[$parent_storage->name][$template_name])) {
2123
                        $extended_type = $storage->template_type_extends[$parent_storage->name][$template_name];
2124
2125
                        if (isset($parent_storage->template_covariants[$i])
2126
                            && !$parent_storage->template_covariants[$i]
2127
                        ) {
2128
                            foreach ($extended_type->getAtomicTypes() as $t) {
2129
                                if ($t instanceof Type\Atomic\TTemplateParam
2130
                                    && $storage->template_types
2131
                                    && $storage->template_covariants
2132
                                    && ($local_offset
2133
                                        = array_search($t->param_name, array_keys($storage->template_types)))
2134
                                        !== false
2135
                                    && !empty($storage->template_covariants[$local_offset])
2136
                                ) {
2137
                                    if (IssueBuffer::accepts(
2138
                                        new InvalidTemplateParam(
2139
                                            'Cannot extend an invariant template param ' . $template_name
2140
                                                . ' into a covariant context',
2141
                                            $code_location
2142
                                        ),
2143
                                        $storage->suppressed_issues + $this->getSuppressedIssues()
2144
                                    )) {
2145
                                        // fall through
2146
                                    }
2147
                                }
2148
                            }
2149
                        }
2150
2151
                        if (!$template_type[0]->isMixed()) {
2152
                            $template_type_copy = clone $template_type[0];
2153
2154
                            $template_result = new \Psalm\Internal\Type\TemplateResult(
2155
                                $previous_extended ?: [],
2156
                                []
2157
                            );
2158
2159
                            $template_type_copy = UnionTemplateHandler::replaceTemplateTypesWithStandins(
2160
                                $template_type_copy,
2161
                                $template_result,
2162
                                $codebase,
2163
                                null,
2164
                                $extended_type,
2165
                                null,
2166
                                null
2167
                            );
2168
2169
                            if (!TypeAnalyzer::isContainedBy($codebase, $extended_type, $template_type_copy)) {
2170
                                if (IssueBuffer::accepts(
2171
                                    new InvalidTemplateParam(
2172
                                        'Extended template param ' . $template_name
2173
                                            . ' expects type ' . $template_type_copy->getId()
2174
                                            . ', type ' . $extended_type->getId() . ' given',
2175
                                        $code_location
2176
                                    ),
2177
                                    $storage->suppressed_issues + $this->getSuppressedIssues()
2178
                                )) {
2179
                                    // fall through
2180
                                }
2181
                            } else {
2182
                                $previous_extended[$template_name] = [
2183
                                    $declaring_class => [$extended_type]
2184
                                ];
2185
                            }
2186
                        } else {
2187
                            $previous_extended[$template_name] = [
2188
                                $declaring_class => [$extended_type]
2189
                            ];
2190
                        }
2191
                    }
2192
                }
2193
2194
                $i++;
2195
            }
2196
        }
2197
    }
2198
}
2199