ClassLikes   F
last analyzed

Complexity

Total Complexity 412

Size/Duplication

Total Lines 2361
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 56

Importance

Changes 0
Metric Value
dl 0
loc 2361
rs 0.8
c 0
b 0
f 0
wmc 412
lcom 1
cbo 56

48 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
A collectPredefinedClassLikes() 0 34 5
A addFullyQualifiedClassName() 0 13 2
A addFullyQualifiedInterfaceName() 0 13 2
A addFullyQualifiedTraitName() 0 13 2
A addFullyQualifiedClassLikeName() 0 6 2
B getMatchingClassLikeNames() 0 32 8
D hasFullyQualifiedClassName() 0 70 18
C hasFullyQualifiedInterfaceName() 0 70 17
B hasFullyQualifiedTraitName() 0 23 6
A classOrInterfaceExists() 0 14 3
A classExists() 0 21 3
A classExtends() 0 18 4
B classImplements() 0 32 10
A interfaceExists() 0 17 2
A interfaceExtends() 0 4 1
A getParentInterfaces() 0 8 1
A traitExists() 0 4 1
A classHasCorrectCasing() 0 12 3
A interfaceHasCorrectCasing() 0 8 2
A traitHasCorrectCase() 0 8 2
A isUserDefined() 0 4 1
A getTraitNode() 0 35 4
A addClassAlias() 0 4 1
A getUnAliasedName() 0 9 2
B consolidateAnalyzedData() 0 41 10
C moveMethods() 0 80 12
C moveProperties() 0 97 14
B moveClassConstants() 0 69 8
F handleClassLikeReferenceInMigration() 0 179 23
F handleDocblockTypeInMigration() 0 125 21
A airliftClassLikeReference() 0 37 3
A airliftClassDefinedDocblockType() 0 34 2
A getConstantsForClass() 0 27 4
B getConstantForClass() 0 56 6
A getLiteralTypeFromScalarValue() 0 16 6
F resolveConstantType() 0 215 53
A setConstantType() 0 16 4
F checkMethodReferences() 0 237 73
F findPossibleMethodParamTypes() 0 108 23
F checkPropertyReferences() 0 119 38
A registerMissingClassLike() 0 4 1
A isMissingClassLike() 0 5 2
A doesClassLikeExist() 0 5 2
A forgetMissingClassLikes() 0 4 1
A removeClassLike() 0 17 1
A getThreadData() 0 12 1
A addThreadData() 0 19 1

How to fix   Complexity   

Complex Class

Complex classes like ClassLikes 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 ClassLikes, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Psalm\Internal\Codebase;
3
4
use function array_merge;
5
use function array_pop;
6
use function count;
7
use function end;
8
use function explode;
9
use function get_declared_classes;
10
use function get_declared_interfaces;
11
use function implode;
12
use const PHP_EOL;
13
use PhpParser;
14
use function preg_match;
15
use function preg_replace;
16
use Psalm\Aliases;
17
use Psalm\CodeLocation;
18
use Psalm\Config;
19
use Psalm\Exception\UnpopulatedClasslikeException;
20
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
21
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
22
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
23
use Psalm\Internal\Provider\ClassLikeStorageProvider;
24
use Psalm\Internal\Provider\FileReferenceProvider;
25
use Psalm\Internal\Provider\StatementsProvider;
26
use Psalm\Internal\Scanner\UnresolvedConstant;
27
use Psalm\Issue\PossiblyUnusedMethod;
28
use Psalm\Issue\PossiblyUnusedParam;
29
use Psalm\Issue\PossiblyUnusedProperty;
30
use Psalm\Issue\UnusedClass;
31
use Psalm\Issue\UnusedMethod;
32
use Psalm\Issue\UnusedProperty;
33
use Psalm\IssueBuffer;
34
use Psalm\Progress\Progress;
35
use Psalm\Progress\VoidProgress;
36
use Psalm\Storage\ClassLikeStorage;
37
use Psalm\Type;
38
use ReflectionProperty;
39
use function strlen;
40
use function strrpos;
41
use function strtolower;
42
use function substr;
43
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
44
45
/**
46
 * @internal
47
 *
48
 * Handles information about classes, interfaces and traits
49
 */
50
class ClassLikes
51
{
52
    /**
53
     * @var ClassLikeStorageProvider
54
     */
55
    private $classlike_storage_provider;
56
57
    /**
58
     * @var FileReferenceProvider
59
     */
60
    public $file_reference_provider;
61
62
    /**
63
     * @var array<lowercase-string, bool>
64
     */
65
    private $existing_classlikes_lc = [];
66
67
    /**
68
     * @var array<lowercase-string, bool>
69
     */
70
    private $existing_classes_lc = [];
71
72
    /**
73
     * @var array<string, bool>
74
     */
75
    private $existing_classes = [];
76
77
    /**
78
     * @var array<lowercase-string, bool>
79
     */
80
    private $existing_interfaces_lc = [];
81
82
    /**
83
     * @var array<string, bool>
84
     */
85
    private $existing_interfaces = [];
86
87
    /**
88
     * @var array<lowercase-string, bool>
89
     */
90
    private $existing_traits_lc = [];
91
92
    /**
93
     * @var array<string, bool>
94
     */
95
    private $existing_traits = [];
96
97
    /**
98
     * @var array<string, string>
99
     */
100
    private $classlike_aliases = [];
101
102
    /**
103
     * @var array<string, PhpParser\Node\Stmt\Trait_>
104
     */
105
    private $trait_nodes = [];
106
107
    /**
108
     * @var bool
109
     */
110
    public $collect_references = false;
111
112
    /**
113
     * @var bool
114
     */
115
    public $collect_locations = false;
116
117
    /**
118
     * @var StatementsProvider
119
     */
120
    private $statements_provider;
121
122
    /**
123
     * @var Config
124
     */
125
    private $config;
126
127
    /**
128
     * @var Scanner
129
     */
130
    private $scanner;
131
132
    public function __construct(
133
        Config $config,
134
        ClassLikeStorageProvider $storage_provider,
135
        FileReferenceProvider $file_reference_provider,
136
        StatementsProvider $statements_provider,
137
        Scanner $scanner
138
    ) {
139
        $this->config = $config;
140
        $this->classlike_storage_provider = $storage_provider;
141
        $this->file_reference_provider = $file_reference_provider;
142
        $this->statements_provider = $statements_provider;
143
        $this->scanner = $scanner;
144
145
        $this->collectPredefinedClassLikes();
146
    }
147
148
    /**
149
     * @return void
150
     */
151
    private function collectPredefinedClassLikes()
152
    {
153
        /** @var array<int, string> */
154
        $predefined_classes = get_declared_classes();
155
156
        foreach ($predefined_classes as $predefined_class) {
157
            $predefined_class = preg_replace('/^\\\/', '', $predefined_class);
158
            /** @psalm-suppress TypeCoercion */
159
            $reflection_class = new \ReflectionClass($predefined_class);
160
161
            if (!$reflection_class->isUserDefined()) {
162
                $predefined_class_lc = strtolower($predefined_class);
163
                $this->existing_classlikes_lc[$predefined_class_lc] = true;
164
                $this->existing_classes_lc[$predefined_class_lc] = true;
165
                $this->existing_classes[$predefined_class] = true;
166
            }
167
        }
168
169
        /** @var array<int, string> */
170
        $predefined_interfaces = get_declared_interfaces();
171
172
        foreach ($predefined_interfaces as $predefined_interface) {
173
            $predefined_interface = preg_replace('/^\\\/', '', $predefined_interface);
174
            /** @psalm-suppress TypeCoercion */
175
            $reflection_class = new \ReflectionClass($predefined_interface);
176
177
            if (!$reflection_class->isUserDefined()) {
178
                $predefined_interface_lc = strtolower($predefined_interface);
179
                $this->existing_classlikes_lc[$predefined_interface_lc] = true;
180
                $this->existing_interfaces_lc[$predefined_interface_lc] = true;
181
                $this->existing_interfaces[$predefined_interface] = true;
182
            }
183
        }
184
    }
185
186
    /**
187
     * @param string        $fq_class_name
188
     * @param string|null   $file_path
189
     *
190
     * @return void
191
     */
192
    public function addFullyQualifiedClassName($fq_class_name, $file_path = null)
193
    {
194
        $fq_class_name_lc = strtolower($fq_class_name);
195
        $this->existing_classlikes_lc[$fq_class_name_lc] = true;
196
        $this->existing_classes_lc[$fq_class_name_lc] = true;
197
        $this->existing_traits_lc[$fq_class_name_lc] = false;
198
        $this->existing_interfaces_lc[$fq_class_name_lc] = false;
199
        $this->existing_classes[$fq_class_name] = true;
200
201
        if ($file_path) {
202
            $this->scanner->setClassLikeFilePath($fq_class_name_lc, $file_path);
203
        }
204
    }
205
206
    /**
207
     * @param string        $fq_class_name
208
     * @param string|null   $file_path
209
     *
210
     * @return void
211
     */
212
    public function addFullyQualifiedInterfaceName($fq_class_name, $file_path = null)
213
    {
214
        $fq_class_name_lc = strtolower($fq_class_name);
215
        $this->existing_classlikes_lc[$fq_class_name_lc] = true;
216
        $this->existing_interfaces_lc[$fq_class_name_lc] = true;
217
        $this->existing_classes_lc[$fq_class_name_lc] = false;
218
        $this->existing_traits_lc[$fq_class_name_lc] = false;
219
        $this->existing_interfaces[$fq_class_name] = true;
220
221
        if ($file_path) {
222
            $this->scanner->setClassLikeFilePath($fq_class_name_lc, $file_path);
223
        }
224
    }
225
226
    /**
227
     * @param string        $fq_class_name
228
     * @param string|null   $file_path
229
     *
230
     * @return void
231
     */
232
    public function addFullyQualifiedTraitName($fq_class_name, $file_path = null)
233
    {
234
        $fq_class_name_lc = strtolower($fq_class_name);
235
        $this->existing_classlikes_lc[$fq_class_name_lc] = true;
236
        $this->existing_traits_lc[$fq_class_name_lc] = true;
237
        $this->existing_classes_lc[$fq_class_name_lc] = false;
238
        $this->existing_interfaces_lc[$fq_class_name_lc] = false;
239
        $this->existing_traits[$fq_class_name] = true;
240
241
        if ($file_path) {
242
            $this->scanner->setClassLikeFilePath($fq_class_name_lc, $file_path);
243
        }
244
    }
245
246
    /**
247
     * @param string        $fq_class_name_lc
248
     * @param string|null   $file_path
249
     *
250
     * @return void
251
     */
252
    public function addFullyQualifiedClassLikeName($fq_class_name_lc, $file_path = null)
253
    {
254
        if ($file_path) {
255
            $this->scanner->setClassLikeFilePath($fq_class_name_lc, $file_path);
256
        }
257
    }
258
259
    /**
260
     * @return string[]
261
     */
262
    public function getMatchingClassLikeNames(string $stub) : array
263
    {
264
        $matching_classes = [];
265
266
        if ($stub[0] === '*') {
267
            $stub = substr($stub, 1);
268
        }
269
270
        $stub = strtolower($stub);
271
272
        foreach ($this->existing_classes as $fq_classlike_name => $found) {
273
            if (!$found) {
274
                continue;
275
            }
276
277
            if (preg_match('@(^|\\\)' . $stub . '.*@i', $fq_classlike_name)) {
278
                $matching_classes[] = $fq_classlike_name;
279
            }
280
        }
281
282
        foreach ($this->existing_interfaces as $fq_classlike_name => $found) {
283
            if (!$found) {
284
                continue;
285
            }
286
287
            if (preg_match('@(^|\\\)' . $stub . '.*@i', $fq_classlike_name)) {
288
                $matching_classes[] = $fq_classlike_name;
289
            }
290
        }
291
292
        return $matching_classes;
293
    }
294
295
    /**
296
     * @param string $fq_class_name
297
     *
298
     * @return bool
299
     */
300
    public function hasFullyQualifiedClassName(
301
        $fq_class_name,
302
        CodeLocation $code_location = null,
303
        ?string $calling_fq_class_name = null,
304
        ?string $calling_method_id = null
305
    ) {
306
        $fq_class_name_lc = strtolower($fq_class_name);
307
308
        if (isset($this->classlike_aliases[$fq_class_name_lc])) {
309
            $fq_class_name_lc = strtolower($this->classlike_aliases[$fq_class_name_lc]);
310
        }
311
312
        if ($code_location) {
313
            if ($calling_method_id) {
314
                $this->file_reference_provider->addMethodReferenceToClass(
315
                    $calling_method_id,
316
                    $fq_class_name_lc
317
                );
318
            } elseif (!$calling_fq_class_name || strtolower($calling_fq_class_name) !== $fq_class_name_lc) {
319
                $this->file_reference_provider->addNonMethodReferenceToClass(
320
                    $code_location->file_path,
321
                    $fq_class_name_lc
322
                );
323
324
                if ($calling_fq_class_name) {
325
                    $class_storage = $this->classlike_storage_provider->get($calling_fq_class_name);
326
327
                    if ($class_storage->location
328
                        && $class_storage->location->file_path !== $code_location->file_path
329
                    ) {
330
                        $this->file_reference_provider->addNonMethodReferenceToClass(
331
                            $class_storage->location->file_path,
332
                            $fq_class_name_lc
333
                        );
334
                    }
335
                }
336
            }
337
        }
338
339
        if (!isset($this->existing_classes_lc[$fq_class_name_lc])
340
            || !$this->existing_classes_lc[$fq_class_name_lc]
341
            || !$this->classlike_storage_provider->has($fq_class_name_lc)
342
        ) {
343
            if ((
344
                !isset($this->existing_classes_lc[$fq_class_name_lc])
345
                    || $this->existing_classes_lc[$fq_class_name_lc] === true
346
                )
347
                && !$this->classlike_storage_provider->has($fq_class_name_lc)
348
            ) {
349
                if (!isset($this->existing_classes_lc[$fq_class_name_lc])) {
350
                    $this->existing_classes_lc[$fq_class_name_lc] = false;
351
352
                    return false;
353
                }
354
355
                return $this->existing_classes_lc[$fq_class_name_lc];
356
            }
357
358
            return false;
359
        }
360
361
        if ($this->collect_locations && $code_location) {
362
            $this->file_reference_provider->addCallingLocationForClass(
363
                $code_location,
364
                strtolower($fq_class_name)
365
            );
366
        }
367
368
        return true;
369
    }
370
371
    /**
372
     * @param string $fq_class_name
373
     *
374
     * @return bool
375
     */
376
    public function hasFullyQualifiedInterfaceName(
377
        $fq_class_name,
378
        CodeLocation $code_location = null,
379
        ?string $calling_fq_class_name = null,
380
        ?string $calling_method_id = null
381
    ) {
382
        $fq_class_name_lc = strtolower($fq_class_name);
383
384
        if (isset($this->classlike_aliases[$fq_class_name_lc])) {
385
            $fq_class_name_lc = strtolower($this->classlike_aliases[$fq_class_name_lc]);
386
        }
387
388
        if (!isset($this->existing_interfaces_lc[$fq_class_name_lc])
389
            || !$this->existing_interfaces_lc[$fq_class_name_lc]
390
            || !$this->classlike_storage_provider->has($fq_class_name_lc)
391
        ) {
392
            if ((
393
                !isset($this->existing_classes_lc[$fq_class_name_lc])
394
                    || $this->existing_classes_lc[$fq_class_name_lc] === true
395
                )
396
                && !$this->classlike_storage_provider->has($fq_class_name_lc)
397
            ) {
398
                if (!isset($this->existing_interfaces_lc[$fq_class_name_lc])) {
399
                    $this->existing_interfaces_lc[$fq_class_name_lc] = false;
400
401
                    return false;
402
                }
403
404
                return $this->existing_interfaces_lc[$fq_class_name_lc];
405
            }
406
407
            return false;
408
        }
409
410
        if ($this->collect_references && $code_location) {
411
            if ($calling_method_id) {
412
                $this->file_reference_provider->addMethodReferenceToClass(
413
                    $calling_method_id,
414
                    $fq_class_name_lc
415
                );
416
            } else {
417
                $this->file_reference_provider->addNonMethodReferenceToClass(
418
                    $code_location->file_path,
419
                    $fq_class_name_lc
420
                );
421
422
                if ($calling_fq_class_name) {
423
                    $class_storage = $this->classlike_storage_provider->get($calling_fq_class_name);
424
425
                    if ($class_storage->location
426
                        && $class_storage->location->file_path !== $code_location->file_path
427
                    ) {
428
                        $this->file_reference_provider->addNonMethodReferenceToClass(
429
                            $class_storage->location->file_path,
430
                            $fq_class_name_lc
431
                        );
432
                    }
433
                }
434
            }
435
        }
436
437
        if ($this->collect_locations && $code_location) {
438
            $this->file_reference_provider->addCallingLocationForClass(
439
                $code_location,
440
                strtolower($fq_class_name)
441
            );
442
        }
443
444
        return true;
445
    }
446
447
    /**
448
     * @param string $fq_class_name
449
     *
450
     * @return bool
451
     */
452
    public function hasFullyQualifiedTraitName($fq_class_name, CodeLocation $code_location = null)
453
    {
454
        $fq_class_name_lc = strtolower($fq_class_name);
455
456
        if (isset($this->classlike_aliases[$fq_class_name_lc])) {
457
            $fq_class_name_lc = strtolower($this->classlike_aliases[$fq_class_name_lc]);
458
        }
459
460
        if (!isset($this->existing_traits_lc[$fq_class_name_lc]) ||
461
            !$this->existing_traits_lc[$fq_class_name_lc]
462
        ) {
463
            return false;
464
        }
465
466
        if ($this->collect_references && $code_location) {
467
            $this->file_reference_provider->addNonMethodReferenceToClass(
468
                $code_location->file_path,
469
                $fq_class_name_lc
470
            );
471
        }
472
473
        return true;
474
    }
475
476
    /**
477
     * Check whether a class/interface exists
478
     *
479
     * @param  string          $fq_class_name
480
     * @param  CodeLocation $code_location
481
     *
482
     * @return bool
483
     */
484
    public function classOrInterfaceExists(
485
        $fq_class_name,
486
        CodeLocation $code_location = null,
487
        ?string $calling_fq_class_name = null,
488
        ?string $calling_method_id = null
489
    ) {
490
        if (!$this->classExists($fq_class_name, $code_location, $calling_fq_class_name, $calling_method_id)
491
            && !$this->interfaceExists($fq_class_name, $code_location, $calling_fq_class_name, $calling_method_id)
492
        ) {
493
            return false;
494
        }
495
496
        return true;
497
    }
498
499
    /**
500
     * Determine whether or not a given class exists
501
     *
502
     * @param  string       $fq_class_name
503
     *
504
     * @return bool
505
     */
506
    public function classExists(
507
        $fq_class_name,
508
        CodeLocation $code_location = null,
509
        ?string $calling_fq_class_name = null,
510
        ?string $calling_method_id = null
511
    ) {
512
        if (isset(ClassLikeAnalyzer::SPECIAL_TYPES[$fq_class_name])) {
513
            return false;
514
        }
515
516
        if ($fq_class_name === 'Generator') {
517
            return true;
518
        }
519
520
        return $this->hasFullyQualifiedClassName(
521
            $fq_class_name,
522
            $code_location,
523
            $calling_fq_class_name,
524
            $calling_method_id
525
        );
526
    }
527
528
    /**
529
     * Determine whether or not a class extends a parent
530
     *
531
     * @param  string       $fq_class_name
532
     * @param  string       $possible_parent
533
     *
534
     * @throws UnpopulatedClasslikeException when called on unpopulated class
535
     * @throws \InvalidArgumentException when class does not exist
536
     *
537
     * @return bool
538
     */
539
    public function classExtends($fq_class_name, $possible_parent, bool $from_api = false)
540
    {
541
        $fq_class_name_lc = strtolower($fq_class_name);
542
543
        if ($fq_class_name_lc === 'generator') {
544
            return false;
545
        }
546
547
        $fq_class_name = $this->classlike_aliases[$fq_class_name_lc] ?? $fq_class_name;
548
549
        $class_storage = $this->classlike_storage_provider->get($fq_class_name_lc);
550
551
        if ($from_api && !$class_storage->populated) {
552
            throw new UnpopulatedClasslikeException($fq_class_name);
553
        }
554
555
        return isset($class_storage->parent_classes[strtolower($possible_parent)]);
556
    }
557
558
    /**
559
     * Check whether a class implements an interface
560
     *
561
     * @param  string       $fq_class_name
562
     * @param  string       $interface
563
     *
564
     * @return bool
565
     */
566
    public function classImplements($fq_class_name, $interface)
567
    {
568
        $interface_id = strtolower($interface);
569
570
        $fq_class_name = strtolower($fq_class_name);
571
572
        if ($interface_id === 'callable' && $fq_class_name === 'closure') {
573
            return true;
574
        }
575
576
        if ($interface_id === 'traversable' && $fq_class_name === 'generator') {
577
            return true;
578
        }
579
580
        if ($interface_id === 'traversable' && $fq_class_name === 'iterator') {
581
            return true;
582
        }
583
584
        if (isset(ClassLikeAnalyzer::SPECIAL_TYPES[$interface_id])
585
            || isset(ClassLikeAnalyzer::SPECIAL_TYPES[$fq_class_name])
586
        ) {
587
            return false;
588
        }
589
590
        if (isset($this->classlike_aliases[$fq_class_name])) {
591
            $fq_class_name = $this->classlike_aliases[$fq_class_name];
592
        }
593
594
        $class_storage = $this->classlike_storage_provider->get($fq_class_name);
595
596
        return isset($class_storage->class_implements[$interface_id]);
597
    }
598
599
    /**
600
     * @param  string         $fq_interface_name
601
     *
602
     * @return bool
603
     */
604
    public function interfaceExists(
605
        $fq_interface_name,
606
        CodeLocation $code_location = null,
607
        ?string $calling_fq_class_name = null,
608
        ?string $calling_method_id = null
609
    ) {
610
        if (isset(ClassLikeAnalyzer::SPECIAL_TYPES[strtolower($fq_interface_name)])) {
611
            return false;
612
        }
613
614
        return $this->hasFullyQualifiedInterfaceName(
615
            $fq_interface_name,
616
            $code_location,
617
            $calling_fq_class_name,
618
            $calling_method_id
619
        );
620
    }
621
622
    /**
623
     * @param  string         $interface_name
624
     * @param  string         $possible_parent
625
     *
626
     * @return bool
627
     */
628
    public function interfaceExtends($interface_name, $possible_parent)
629
    {
630
        return isset($this->getParentInterfaces($interface_name)[strtolower($possible_parent)]);
631
    }
632
633
    /**
634
     * @param  string         $fq_interface_name
635
     *
636
     * @return array<string, string>   all interfaces extended by $interface_name
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
637
     */
638
    public function getParentInterfaces($fq_interface_name)
639
    {
640
        $fq_interface_name = strtolower($fq_interface_name);
641
642
        $storage = $this->classlike_storage_provider->get($fq_interface_name);
643
644
        return $storage->parent_interfaces;
645
    }
646
647
    /**
648
     * @param  string         $fq_trait_name
649
     *
650
     * @return bool
651
     */
652
    public function traitExists($fq_trait_name, CodeLocation $code_location = null)
653
    {
654
        return $this->hasFullyQualifiedTraitName($fq_trait_name, $code_location);
655
    }
656
657
    /**
658
     * Determine whether or not a class has the correct casing
659
     *
660
     * @param  string $fq_class_name
661
     *
662
     * @return bool
663
     */
664
    public function classHasCorrectCasing($fq_class_name)
665
    {
666
        if ($fq_class_name === 'Generator') {
667
            return true;
668
        }
669
670
        if (isset($this->classlike_aliases[strtolower($fq_class_name)])) {
671
            return true;
672
        }
673
674
        return isset($this->existing_classes[$fq_class_name]);
675
    }
676
677
    /**
678
     * @param  string $fq_interface_name
679
     *
680
     * @return bool
681
     */
682
    public function interfaceHasCorrectCasing($fq_interface_name)
683
    {
684
        if (isset($this->classlike_aliases[strtolower($fq_interface_name)])) {
685
            return true;
686
        }
687
688
        return isset($this->existing_interfaces[$fq_interface_name]);
689
    }
690
691
    /**
692
     * @param  string $fq_trait_name
693
     *
694
     * @return bool
695
     */
696
    public function traitHasCorrectCase($fq_trait_name)
697
    {
698
        if (isset($this->classlike_aliases[strtolower($fq_trait_name)])) {
699
            return true;
700
        }
701
702
        return isset($this->existing_traits[$fq_trait_name]);
703
    }
704
705
    /**
706
     * @param  lowercase-string  $fq_class_name
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
707
     *
708
     * @return bool
709
     */
710
    public function isUserDefined($fq_class_name)
711
    {
712
        return $this->classlike_storage_provider->get($fq_class_name)->user_defined;
713
    }
714
715
    /**
716
     * @param  string $fq_trait_name
717
     *
718
     * @return PhpParser\Node\Stmt\Trait_
719
     */
720
    public function getTraitNode($fq_trait_name)
721
    {
722
        $fq_trait_name_lc = strtolower($fq_trait_name);
723
724
        if (isset($this->trait_nodes[$fq_trait_name_lc])) {
725
            return $this->trait_nodes[$fq_trait_name_lc];
726
        }
727
728
        $storage = $this->classlike_storage_provider->get($fq_trait_name);
729
730
        if (!$storage->location) {
731
            throw new \UnexpectedValueException('Storage should exist for ' . $fq_trait_name);
732
        }
733
734
        $file_statements = $this->statements_provider->getStatementsForFile($storage->location->file_path);
735
736
        $trait_finder = new \Psalm\Internal\PhpVisitor\TraitFinder($fq_trait_name);
737
738
        $traverser = new \PhpParser\NodeTraverser();
739
        $traverser->addVisitor(
740
            $trait_finder
741
        );
742
743
        $traverser->traverse($file_statements);
744
745
        $trait_node = $trait_finder->getNode();
746
747
        if ($trait_node) {
748
            $this->trait_nodes[$fq_trait_name_lc] = $trait_node;
749
750
            return $trait_node;
751
        }
752
753
        throw new \UnexpectedValueException('Could not locate trait statement');
754
    }
755
756
    /**
757
     * @param lowercase-string $alias_name
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
758
     * @return void
759
     */
760
    public function addClassAlias(string $fq_class_name, string $alias_name)
761
    {
762
        $this->classlike_aliases[$alias_name] = $fq_class_name;
763
    }
764
765
    /**
766
     * @return string
767
     */
768
    public function getUnAliasedName(string $alias_name)
769
    {
770
        $alias_name_lc = strtolower($alias_name);
771
        if ($this->existing_classlikes_lc[$alias_name_lc] ?? false) {
772
            return $alias_name;
773
        }
774
775
        return $this->classlike_aliases[$alias_name_lc] ?? $alias_name;
776
    }
777
778
    /**
779
     * @return void
780
     */
781
    public function consolidateAnalyzedData(Methods $methods, ?Progress $progress, bool $find_unused_code)
782
    {
783
        if ($progress === null) {
784
            $progress = new VoidProgress();
785
        }
786
787
        $progress->debug('Checking class references' . PHP_EOL);
788
789
        foreach ($this->existing_classlikes_lc as $fq_class_name_lc => $_) {
790
            try {
791
                $classlike_storage = $this->classlike_storage_provider->get($fq_class_name_lc);
792
            } catch (\InvalidArgumentException $e) {
793
                continue;
794
            }
795
796
            if ($classlike_storage->location
797
                && $this->config->isInProjectDirs($classlike_storage->location->file_path)
798
                && !$classlike_storage->is_trait
799
            ) {
800
                if ($find_unused_code) {
801
                    if (!$this->file_reference_provider->isClassReferenced($fq_class_name_lc)) {
802
                        if (IssueBuffer::accepts(
803
                            new UnusedClass(
804
                                'Class ' . $classlike_storage->name . ' is never used',
805
                                $classlike_storage->location,
806
                                $classlike_storage->name
807
                            ),
808
                            $classlike_storage->suppressed_issues
809
                        )) {
810
                            // fall through
811
                        }
812
                    } else {
813
                        $this->checkMethodReferences($classlike_storage, $methods);
814
                        $this->checkPropertyReferences($classlike_storage);
815
                    }
816
                }
817
818
                $this->findPossibleMethodParamTypes($classlike_storage);
819
            }
820
        }
821
    }
822
823
    /**
824
     * @return void
825
     */
826
    public function moveMethods(Methods $methods, Progress $progress = null)
827
    {
828
        if ($progress === null) {
829
            $progress = new VoidProgress();
830
        }
831
832
        $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
833
        $codebase = $project_analyzer->getCodebase();
834
835
        if (!$codebase->methods_to_move) {
836
            return;
837
        }
838
839
        $progress->debug('Refactoring methods ' . PHP_EOL);
840
841
        $code_migrations = [];
842
843
        foreach ($codebase->methods_to_move as $source => $destination) {
844
            $source_parts = explode('::', $source);
845
846
            try {
847
                $source_method_storage = $methods->getStorage(
848
                    new \Psalm\Internal\MethodIdentifier(...$source_parts)
0 ignored issues
show
Bug introduced by
The call to MethodIdentifier::__construct() misses a required argument $method_name.

This check looks for function calls that miss required arguments.

Loading history...
Documentation introduced by
$source_parts is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
849
                );
850
            } catch (\InvalidArgumentException $e) {
851
                continue;
852
            }
853
854
            list($destination_fq_class_name, $destination_name) = explode('::', $destination);
855
856
            try {
857
                $classlike_storage = $this->classlike_storage_provider->get($destination_fq_class_name);
858
            } catch (\InvalidArgumentException $e) {
859
                continue;
860
            }
861
862
            if ($classlike_storage->stmt_location
863
                && $this->config->isInProjectDirs($classlike_storage->stmt_location->file_path)
864
                && $source_method_storage->stmt_location
865
                && $source_method_storage->stmt_location->file_path
866
                && $source_method_storage->location
867
            ) {
868
                $new_class_bounds = $classlike_storage->stmt_location->getSnippetBounds();
869
                $old_method_bounds = $source_method_storage->stmt_location->getSnippetBounds();
870
871
                $old_method_name_bounds = $source_method_storage->location->getSelectionBounds();
872
873
                FileManipulationBuffer::add(
874
                    $source_method_storage->stmt_location->file_path,
875
                    [
876
                        new \Psalm\FileManipulation(
877
                            $old_method_name_bounds[0],
878
                            $old_method_name_bounds[1],
879
                            $destination_name
880
                        ),
881
                    ]
882
                );
883
884
                $selection = $classlike_storage->stmt_location->getSnippet();
885
886
                $insert_pos = strrpos($selection, "\n", -1);
887
888
                if (!$insert_pos) {
889
                    $insert_pos = strlen($selection) - 1;
890
                } else {
891
                    ++$insert_pos;
892
                }
893
894
                $code_migrations[] = new \Psalm\Internal\FileManipulation\CodeMigration(
895
                    $source_method_storage->stmt_location->file_path,
896
                    $old_method_bounds[0],
897
                    $old_method_bounds[1],
898
                    $classlike_storage->stmt_location->file_path,
899
                    $new_class_bounds[0] + $insert_pos
900
                );
901
            }
902
        }
903
904
        FileManipulationBuffer::addCodeMigrations($code_migrations);
905
    }
906
907
    /**
908
     * @return void
909
     */
910
    public function moveProperties(Properties $properties, Progress $progress = null)
911
    {
912
        if ($progress === null) {
913
            $progress = new VoidProgress();
914
        }
915
916
        $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
917
        $codebase = $project_analyzer->getCodebase();
918
919
        if (!$codebase->properties_to_move) {
920
            return;
921
        }
922
923
        $progress->debug('Refacting properties ' . PHP_EOL);
924
925
        $code_migrations = [];
926
927
        foreach ($codebase->properties_to_move as $source => $destination) {
928
            try {
929
                $source_property_storage = $properties->getStorage($source);
930
            } catch (\InvalidArgumentException $e) {
931
                continue;
932
            }
933
934
            list($source_fq_class_name) = explode('::$', $source);
935
            list($destination_fq_class_name, $destination_name) = explode('::$', $destination);
936
937
            $source_classlike_storage = $this->classlike_storage_provider->get($source_fq_class_name);
938
            $destination_classlike_storage = $this->classlike_storage_provider->get($destination_fq_class_name);
939
940
            if ($destination_classlike_storage->stmt_location
941
                && $this->config->isInProjectDirs($destination_classlike_storage->stmt_location->file_path)
942
                && $source_property_storage->stmt_location
943
                && $source_property_storage->stmt_location->file_path
944
                && $source_property_storage->location
945
            ) {
946
                if ($source_property_storage->type
947
                    && $source_property_storage->type_location
948
                    && $source_property_storage->type_location !== $source_property_storage->signature_type_location
949
                ) {
950
                    $bounds = $source_property_storage->type_location->getSelectionBounds();
951
952
                    $replace_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
953
                        $codebase,
954
                        $source_property_storage->type,
955
                        $source_classlike_storage->name,
956
                        $source_classlike_storage->name,
957
                        $source_classlike_storage->parent_class
958
                    );
959
960
                    $this->airliftClassDefinedDocblockType(
961
                        $replace_type,
962
                        $destination_fq_class_name,
963
                        $source_property_storage->stmt_location->file_path,
964
                        $bounds[0],
965
                        $bounds[1]
966
                    );
967
                }
968
969
                $new_class_bounds = $destination_classlike_storage->stmt_location->getSnippetBounds();
970
                $old_property_bounds = $source_property_storage->stmt_location->getSnippetBounds();
971
972
                $old_property_name_bounds = $source_property_storage->location->getSelectionBounds();
973
974
                FileManipulationBuffer::add(
975
                    $source_property_storage->stmt_location->file_path,
976
                    [
977
                        new \Psalm\FileManipulation(
978
                            $old_property_name_bounds[0],
979
                            $old_property_name_bounds[1],
980
                            '$' . $destination_name
981
                        ),
982
                    ]
983
                );
984
985
                $selection = $destination_classlike_storage->stmt_location->getSnippet();
986
987
                $insert_pos = strrpos($selection, "\n", -1);
988
989
                if (!$insert_pos) {
990
                    $insert_pos = strlen($selection) - 1;
991
                } else {
992
                    ++$insert_pos;
993
                }
994
995
                $code_migrations[] = new \Psalm\Internal\FileManipulation\CodeMigration(
996
                    $source_property_storage->stmt_location->file_path,
997
                    $old_property_bounds[0],
998
                    $old_property_bounds[1],
999
                    $destination_classlike_storage->stmt_location->file_path,
1000
                    $new_class_bounds[0] + $insert_pos
1001
                );
1002
            }
1003
        }
1004
1005
        FileManipulationBuffer::addCodeMigrations($code_migrations);
1006
    }
1007
1008
    /**
1009
     * @return void
1010
     */
1011
    public function moveClassConstants(Progress $progress = null)
1012
    {
1013
        if ($progress === null) {
1014
            $progress = new VoidProgress();
1015
        }
1016
1017
        $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
1018
        $codebase = $project_analyzer->getCodebase();
1019
1020
        if (!$codebase->class_constants_to_move) {
1021
            return;
1022
        }
1023
1024
        $progress->debug('Refacting constants ' . PHP_EOL);
1025
1026
        $code_migrations = [];
1027
1028
        foreach ($codebase->class_constants_to_move as $source => $destination) {
1029
            list($source_fq_class_name, $source_const_name) = explode('::', $source);
1030
            list($destination_fq_class_name, $destination_name) = explode('::', $destination);
1031
1032
            $source_classlike_storage = $this->classlike_storage_provider->get($source_fq_class_name);
1033
            $destination_classlike_storage = $this->classlike_storage_provider->get($destination_fq_class_name);
1034
1035
            $source_const_stmt_location = $source_classlike_storage->class_constant_stmt_locations[$source_const_name];
1036
            $source_const_location = $source_classlike_storage->class_constant_locations[$source_const_name];
1037
1038
            if ($destination_classlike_storage->stmt_location
1039
                && $this->config->isInProjectDirs($destination_classlike_storage->stmt_location->file_path)
1040
                && $source_const_stmt_location->file_path
1041
            ) {
1042
                $new_class_bounds = $destination_classlike_storage->stmt_location->getSnippetBounds();
1043
                $old_const_bounds = $source_const_stmt_location->getSnippetBounds();
1044
1045
                $old_const_name_bounds = $source_const_location->getSelectionBounds();
1046
1047
                FileManipulationBuffer::add(
1048
                    $source_const_stmt_location->file_path,
1049
                    [
1050
                        new \Psalm\FileManipulation(
1051
                            $old_const_name_bounds[0],
1052
                            $old_const_name_bounds[1],
1053
                            $destination_name
1054
                        ),
1055
                    ]
1056
                );
1057
1058
                $selection = $destination_classlike_storage->stmt_location->getSnippet();
1059
1060
                $insert_pos = strrpos($selection, "\n", -1);
1061
1062
                if (!$insert_pos) {
1063
                    $insert_pos = strlen($selection) - 1;
1064
                } else {
1065
                    ++$insert_pos;
1066
                }
1067
1068
                $code_migrations[] = new \Psalm\Internal\FileManipulation\CodeMigration(
1069
                    $source_const_stmt_location->file_path,
1070
                    $old_const_bounds[0],
1071
                    $old_const_bounds[1],
1072
                    $destination_classlike_storage->stmt_location->file_path,
1073
                    $new_class_bounds[0] + $insert_pos
1074
                );
1075
            }
1076
        }
1077
1078
        FileManipulationBuffer::addCodeMigrations($code_migrations);
1079
    }
1080
1081
    /**
1082
     * @param lowercase-string|null $calling_method_id
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string|null could not be parsed: Unknown type name "lowercase-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1083
     */
1084
    public function handleClassLikeReferenceInMigration(
1085
        \Psalm\Codebase $codebase,
1086
        \Psalm\StatementsSource $source,
1087
        PhpParser\Node $class_name_node,
1088
        string $fq_class_name,
1089
        ?string $calling_method_id,
1090
        bool $force_change = false,
1091
        bool $was_self = false
1092
    ) : bool {
1093
        $calling_fq_class_name = $source->getFQCLN();
1094
1095
        // if we're inside a moved class static method
1096
        if ($codebase->methods_to_move
1097
            && $calling_fq_class_name
1098
            && $calling_method_id
1099
            && isset($codebase->methods_to_move[$calling_method_id])
1100
        ) {
1101
            $destination_class = explode('::', $codebase->methods_to_move[$calling_method_id])[0];
1102
1103
            $intended_fq_class_name = strtolower($calling_fq_class_name) === strtolower($fq_class_name)
1104
                && isset($codebase->classes_to_move[strtolower($calling_fq_class_name)])
1105
                ? $destination_class
1106
                : $fq_class_name;
1107
1108
            $this->airliftClassLikeReference(
1109
                $intended_fq_class_name,
1110
                $destination_class,
1111
                $source->getFilePath(),
1112
                (int) $class_name_node->getAttribute('startFilePos'),
1113
                (int) $class_name_node->getAttribute('endFilePos') + 1,
1114
                $class_name_node instanceof PhpParser\Node\Scalar\MagicConst\Class_,
1115
                $was_self
1116
            );
1117
1118
            return true;
1119
        }
1120
1121
        // if we're outside a moved class, but we're changing all references to a class
1122
        if (isset($codebase->class_transforms[strtolower($fq_class_name)])) {
1123
            $new_fq_class_name = $codebase->class_transforms[strtolower($fq_class_name)];
1124
            $file_manipulations = [];
1125
1126
            if ($class_name_node instanceof PhpParser\Node\Identifier) {
1127
                $destination_parts = explode('\\', $new_fq_class_name);
1128
1129
                $destination_class_name = array_pop($destination_parts);
1130
                $file_manipulations = [];
1131
1132
                $file_manipulations[] = new \Psalm\FileManipulation(
1133
                    (int) $class_name_node->getAttribute('startFilePos'),
1134
                    (int) $class_name_node->getAttribute('endFilePos') + 1,
1135
                    $destination_class_name
1136
                );
1137
1138
                FileManipulationBuffer::add($source->getFilePath(), $file_manipulations);
1139
1140
                return true;
1141
            }
1142
1143
            $uses_flipped = $source->getAliasedClassesFlipped();
1144
            $uses_flipped_replaceable = $source->getAliasedClassesFlippedReplaceable();
1145
1146
            $old_fq_class_name = strtolower($fq_class_name);
1147
1148
            $migrated_source_fqcln = $calling_fq_class_name;
1149
1150
            if ($calling_fq_class_name
1151
                && isset($codebase->class_transforms[strtolower($calling_fq_class_name)])
1152
            ) {
1153
                $migrated_source_fqcln = $codebase->class_transforms[strtolower($calling_fq_class_name)];
1154
            }
1155
1156
            $source_namespace = $source->getNamespace();
1157
1158
            if ($migrated_source_fqcln && $calling_fq_class_name !== $migrated_source_fqcln) {
1159
                $new_source_parts = explode('\\', $migrated_source_fqcln);
1160
                array_pop($new_source_parts);
1161
                $source_namespace = implode('\\', $new_source_parts);
1162
            }
1163
1164
            if (isset($uses_flipped_replaceable[$old_fq_class_name])) {
1165
                $alias = $uses_flipped_replaceable[$old_fq_class_name];
1166
                unset($uses_flipped[$old_fq_class_name]);
1167
                $old_class_name_parts = explode('\\', $old_fq_class_name);
1168
                $old_class_name = end($old_class_name_parts);
1169
                if (strtolower($old_class_name) === strtolower($alias)) {
1170
                    $new_class_name_parts = explode('\\', $new_fq_class_name);
1171
                    $new_class_name = end($new_class_name_parts);
1172
                    $uses_flipped[strtolower($new_fq_class_name)] = $new_class_name;
1173
                } else {
1174
                    $uses_flipped[strtolower($new_fq_class_name)] = $alias;
1175
                }
1176
            }
1177
1178
            $file_manipulations[] = new \Psalm\FileManipulation(
1179
                (int) $class_name_node->getAttribute('startFilePos'),
1180
                (int) $class_name_node->getAttribute('endFilePos') + 1,
1181
                Type::getStringFromFQCLN(
1182
                    $new_fq_class_name,
1183
                    $source_namespace,
1184
                    $uses_flipped,
1185
                    $migrated_source_fqcln,
1186
                    $was_self
1187
                )
1188
                    . ($class_name_node instanceof PhpParser\Node\Scalar\MagicConst\Class_ ? '::class' : '')
1189
            );
1190
1191
            FileManipulationBuffer::add($source->getFilePath(), $file_manipulations);
1192
1193
            return true;
1194
        }
1195
1196
        // if we're inside a moved class (could be a method, could be a property/class const default)
1197
        if ($codebase->classes_to_move
1198
            && $calling_fq_class_name
1199
            && isset($codebase->classes_to_move[strtolower($calling_fq_class_name)])
1200
        ) {
1201
            $destination_class = $codebase->classes_to_move[strtolower($calling_fq_class_name)];
1202
1203
            if ($class_name_node instanceof PhpParser\Node\Identifier) {
1204
                $destination_parts = explode('\\', $destination_class);
1205
1206
                $destination_class_name = array_pop($destination_parts);
1207
                $file_manipulations = [];
1208
1209
                $file_manipulations[] = new \Psalm\FileManipulation(
1210
                    (int) $class_name_node->getAttribute('startFilePos'),
1211
                    (int) $class_name_node->getAttribute('endFilePos') + 1,
1212
                    $destination_class_name
1213
                );
1214
1215
                FileManipulationBuffer::add($source->getFilePath(), $file_manipulations);
1216
            } else {
1217
                $this->airliftClassLikeReference(
1218
                    strtolower($calling_fq_class_name) === strtolower($fq_class_name)
1219
                        ? $destination_class
1220
                        : $fq_class_name,
1221
                    $destination_class,
1222
                    $source->getFilePath(),
1223
                    (int) $class_name_node->getAttribute('startFilePos'),
1224
                    (int) $class_name_node->getAttribute('endFilePos') + 1,
1225
                    $class_name_node instanceof PhpParser\Node\Scalar\MagicConst\Class_
1226
                );
1227
            }
1228
1229
            return true;
1230
        }
1231
1232
        if ($force_change) {
1233
            if ($calling_fq_class_name) {
1234
                $this->airliftClassLikeReference(
1235
                    $fq_class_name,
1236
                    $calling_fq_class_name,
1237
                    $source->getFilePath(),
1238
                    (int) $class_name_node->getAttribute('startFilePos'),
1239
                    (int) $class_name_node->getAttribute('endFilePos') + 1
1240
                );
1241
            } else {
1242
                $file_manipulations = [];
1243
1244
                $file_manipulations[] = new \Psalm\FileManipulation(
1245
                    (int) $class_name_node->getAttribute('startFilePos'),
1246
                    (int) $class_name_node->getAttribute('endFilePos') + 1,
1247
                    Type::getStringFromFQCLN(
1248
                        $fq_class_name,
1249
                        $source->getNamespace(),
1250
                        $source->getAliasedClassesFlipped(),
1251
                        null
1252
                    )
1253
                );
1254
1255
                FileManipulationBuffer::add($source->getFilePath(), $file_manipulations);
1256
            }
1257
1258
            return true;
1259
        }
1260
1261
        return false;
1262
    }
1263
1264
    /**
1265
     * @param lowercase-string|null $calling_method_id
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string|null could not be parsed: Unknown type name "lowercase-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1266
     */
1267
    public function handleDocblockTypeInMigration(
1268
        \Psalm\Codebase $codebase,
1269
        \Psalm\StatementsSource $source,
1270
        Type\Union $type,
1271
        CodeLocation $type_location,
1272
        ?string $calling_method_id
1273
    ) : void {
1274
        $calling_fq_class_name = $source->getFQCLN();
1275
1276
        $moved_type = false;
1277
1278
        // if we're inside a moved class static method
1279
        if ($codebase->methods_to_move
1280
            && $calling_fq_class_name
1281
            && $calling_method_id
1282
            && isset($codebase->methods_to_move[$calling_method_id])
1283
        ) {
1284
            $bounds = $type_location->getSelectionBounds();
1285
1286
            $destination_class = explode('::', $codebase->methods_to_move[$calling_method_id])[0];
1287
1288
            $this->airliftClassDefinedDocblockType(
1289
                $type,
1290
                $destination_class,
1291
                $source->getFilePath(),
1292
                $bounds[0],
1293
                $bounds[1]
1294
            );
1295
1296
            $moved_type = true;
1297
        }
1298
1299
        // if we're outside a moved class, but we're changing all references to a class
1300
        if (!$moved_type && $codebase->class_transforms) {
1301
            $uses_flipped = $source->getAliasedClassesFlipped();
1302
            $uses_flipped_replaceable = $source->getAliasedClassesFlippedReplaceable();
1303
1304
            $migrated_source_fqcln = $calling_fq_class_name;
1305
1306
            if ($calling_fq_class_name
1307
                && isset($codebase->class_transforms[strtolower($calling_fq_class_name)])
1308
            ) {
1309
                $migrated_source_fqcln = $codebase->class_transforms[strtolower($calling_fq_class_name)];
1310
            }
1311
1312
            $source_namespace = $source->getNamespace();
1313
1314
            if ($migrated_source_fqcln && $calling_fq_class_name !== $migrated_source_fqcln) {
1315
                $new_source_parts = explode('\\', $migrated_source_fqcln);
1316
                array_pop($new_source_parts);
1317
                $source_namespace = implode('\\', $new_source_parts);
1318
            }
1319
1320
            foreach ($codebase->class_transforms as $old_fq_class_name => $new_fq_class_name) {
1321
                if (isset($uses_flipped_replaceable[$old_fq_class_name])) {
1322
                    $alias = $uses_flipped_replaceable[$old_fq_class_name];
1323
                    unset($uses_flipped[$old_fq_class_name]);
1324
                    $old_class_name_parts = explode('\\', $old_fq_class_name);
1325
                    $old_class_name = end($old_class_name_parts);
1326
                    if (strtolower($old_class_name) === strtolower($alias)) {
1327
                        $new_class_name_parts = explode('\\', $new_fq_class_name);
1328
                        $new_class_name = end($new_class_name_parts);
1329
                        $uses_flipped[strtolower($new_fq_class_name)] = $new_class_name;
1330
                    } else {
1331
                        $uses_flipped[strtolower($new_fq_class_name)] = $alias;
1332
                    }
1333
                }
1334
            }
1335
1336
            foreach ($codebase->class_transforms as $old_fq_class_name => $new_fq_class_name) {
1337
                if ($type->containsClassLike($old_fq_class_name)) {
1338
                    $type = clone $type;
1339
1340
                    $type->replaceClassLike($old_fq_class_name, $new_fq_class_name);
1341
1342
                    $bounds = $type_location->getSelectionBounds();
1343
1344
                    $file_manipulations = [];
1345
1346
                    $file_manipulations[] = new \Psalm\FileManipulation(
1347
                        $bounds[0],
1348
                        $bounds[1],
1349
                        $type->toNamespacedString(
1350
                            $source_namespace,
1351
                            $uses_flipped,
1352
                            $migrated_source_fqcln,
1353
                            false
1354
                        )
1355
                    );
1356
1357
                    FileManipulationBuffer::add(
1358
                        $source->getFilePath(),
1359
                        $file_manipulations
1360
                    );
1361
1362
                    $moved_type = true;
1363
                }
1364
            }
1365
        }
1366
1367
        // if we're inside a moved class (could be a method, could be a property/class const default)
1368
        if (!$moved_type
1369
            && $codebase->classes_to_move
1370
            && $calling_fq_class_name
1371
            && isset($codebase->classes_to_move[strtolower($calling_fq_class_name)])
1372
        ) {
1373
            $bounds = $type_location->getSelectionBounds();
1374
1375
            $destination_class = $codebase->classes_to_move[strtolower($calling_fq_class_name)];
1376
1377
            if ($type->containsClassLike(strtolower($calling_fq_class_name))) {
1378
                $type = clone $type;
1379
1380
                $type->replaceClassLike(strtolower($calling_fq_class_name), $destination_class);
1381
            }
1382
1383
            $this->airliftClassDefinedDocblockType(
1384
                $type,
1385
                $destination_class,
1386
                $source->getFilePath(),
1387
                $bounds[0],
1388
                $bounds[1]
1389
            );
1390
        }
1391
    }
1392
1393
    public function airliftClassLikeReference(
1394
        string $fq_class_name,
1395
        string $destination_fq_class_name,
1396
        string $source_file_path,
1397
        int $source_start,
1398
        int $source_end,
1399
        bool $add_class_constant = false,
1400
        bool $allow_self = false
1401
    ) : void {
1402
        $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
1403
        $codebase = $project_analyzer->getCodebase();
1404
1405
        $destination_class_storage = $codebase->classlike_storage_provider->get($destination_fq_class_name);
1406
1407
        if (!$destination_class_storage->aliases) {
1408
            throw new \UnexpectedValueException('Aliases should not be null');
1409
        }
1410
1411
        $file_manipulations = [];
1412
1413
        $file_manipulations[] = new \Psalm\FileManipulation(
1414
            $source_start,
1415
            $source_end,
1416
            Type::getStringFromFQCLN(
1417
                $fq_class_name,
1418
                $destination_class_storage->aliases->namespace,
1419
                $destination_class_storage->aliases->uses_flipped,
1420
                $destination_class_storage->name,
1421
                $allow_self
1422
            ) . ($add_class_constant ? '::class' : '')
1423
        );
1424
1425
        FileManipulationBuffer::add(
1426
            $source_file_path,
1427
            $file_manipulations
1428
        );
1429
    }
1430
1431
    public function airliftClassDefinedDocblockType(
1432
        Type\Union $type,
1433
        string $destination_fq_class_name,
1434
        string $source_file_path,
1435
        int $source_start,
1436
        int $source_end
1437
    ) : void {
1438
        $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
1439
        $codebase = $project_analyzer->getCodebase();
1440
1441
        $destination_class_storage = $codebase->classlike_storage_provider->get($destination_fq_class_name);
1442
1443
        if (!$destination_class_storage->aliases) {
1444
            throw new \UnexpectedValueException('Aliases should not be null');
1445
        }
1446
1447
        $file_manipulations = [];
1448
1449
        $file_manipulations[] = new \Psalm\FileManipulation(
1450
            $source_start,
1451
            $source_end,
1452
            $type->toNamespacedString(
1453
                $destination_class_storage->aliases->namespace,
1454
                $destination_class_storage->aliases->uses_flipped,
1455
                $destination_class_storage->name,
1456
                false
1457
            )
1458
        );
1459
1460
        FileManipulationBuffer::add(
1461
            $source_file_path,
1462
            $file_manipulations
1463
        );
1464
    }
1465
1466
    /**
1467
     * @param  string $class_name
1468
     * @param  mixed  $visibility
1469
     *
1470
     * @return array<string,Type\Union>
1471
     */
1472
    public function getConstantsForClass($class_name, $visibility)
1473
    {
1474
        $class_name = strtolower($class_name);
1475
1476
        $storage = $this->classlike_storage_provider->get($class_name);
1477
1478
        if ($visibility === ReflectionProperty::IS_PUBLIC) {
1479
            return $storage->public_class_constants;
1480
        }
1481
1482
        if ($visibility === ReflectionProperty::IS_PROTECTED) {
1483
            return array_merge(
1484
                $storage->public_class_constants,
1485
                $storage->protected_class_constants
1486
            );
1487
        }
1488
1489
        if ($visibility === ReflectionProperty::IS_PRIVATE) {
1490
            return array_merge(
1491
                $storage->public_class_constants,
1492
                $storage->protected_class_constants,
1493
                $storage->private_class_constants
1494
            );
1495
        }
1496
1497
        throw new \InvalidArgumentException('Must specify $visibility');
1498
    }
1499
1500
    public function getConstantForClass(
1501
        string $class_name,
1502
        string $constant_name,
1503
        int $visibility,
1504
        ?\Psalm\Internal\Analyzer\StatementsAnalyzer $statements_analyzer = null,
1505
        array $visited_constant_ids = []
1506
    ) : ?Type\Union {
1507
        $class_name = strtolower($class_name);
1508
        $storage = $this->classlike_storage_provider->get($class_name);
1509
1510
        if ($visibility === ReflectionProperty::IS_PUBLIC) {
1511
            $type_candidates = $storage->public_class_constants;
1512
1513
            $fallbacks = $storage->public_class_constant_nodes;
1514
        } elseif ($visibility === ReflectionProperty::IS_PROTECTED) {
1515
            $type_candidates = array_merge(
1516
                $storage->public_class_constants,
1517
                $storage->protected_class_constants
1518
            );
1519
1520
            $fallbacks = array_merge(
1521
                $storage->public_class_constant_nodes,
1522
                $storage->protected_class_constant_nodes
1523
            );
1524
        } elseif ($visibility === ReflectionProperty::IS_PRIVATE) {
1525
            $type_candidates = array_merge(
1526
                $storage->public_class_constants,
1527
                $storage->protected_class_constants,
1528
                $storage->private_class_constants
1529
            );
1530
1531
            $fallbacks = array_merge(
1532
                $storage->public_class_constant_nodes,
1533
                $storage->protected_class_constant_nodes,
1534
                $storage->private_class_constant_nodes
1535
            );
1536
        } else {
1537
            throw new \InvalidArgumentException('Must specify $visibility');
1538
        }
1539
1540
        if (isset($fallbacks[$constant_name])) {
1541
            return new Type\Union([
1542
                $this->resolveConstantType(
1543
                    $fallbacks[$constant_name],
1544
                    $statements_analyzer,
1545
                    $visited_constant_ids
1546
                )
1547
            ]);
1548
        }
1549
1550
        if (isset($type_candidates[$constant_name])) {
1551
            return $type_candidates[$constant_name];
1552
        }
1553
1554
        return null;
1555
    }
1556
1557
    /**
1558
     * @param  string|int|float|bool|null $value
1559
     */
1560
    private static function getLiteralTypeFromScalarValue($value) : Type\Atomic
1561
    {
1562
        if (\is_string($value)) {
1563
            return new Type\Atomic\TLiteralString($value);
1564
        } elseif (\is_int($value)) {
1565
            return new Type\Atomic\TLiteralInt($value);
1566
        } elseif (\is_float($value)) {
1567
            return new Type\Atomic\TLiteralFloat($value);
1568
        } elseif ($value === false) {
1569
            return new Type\Atomic\TFalse;
1570
        } elseif ($value === true) {
1571
            return new Type\Atomic\TTrue;
1572
        } else {
1573
            return new Type\Atomic\TNull;
1574
        }
1575
    }
1576
1577
    private function resolveConstantType(
1578
        \Psalm\Internal\Scanner\UnresolvedConstantComponent $c,
1579
        \Psalm\Internal\Analyzer\StatementsAnalyzer $statements_analyzer = null,
1580
        array $visited_constant_ids = []
1581
    ) : Type\Atomic {
1582
        $c_id = \spl_object_id($c);
1583
1584
        if (isset($visited_constant_ids[$c_id])) {
1585
            throw new \Psalm\Exception\CircularReferenceException('Found a circular reference');
1586
        }
1587
1588
        if ($c instanceof UnresolvedConstant\ScalarValue) {
1589
            return self::getLiteralTypeFromScalarValue($c->value);
1590
        }
1591
1592
        if ($c instanceof UnresolvedConstant\UnresolvedBinaryOp) {
1593
            $left = $this->resolveConstantType(
1594
                $c->left,
1595
                $statements_analyzer,
1596
                $visited_constant_ids + [$c_id => true]
1597
            );
1598
            $right = $this->resolveConstantType(
1599
                $c->right,
1600
                $statements_analyzer,
1601
                $visited_constant_ids + [$c_id => true]
1602
            );
1603
1604
            if ($left instanceof Type\Atomic\TMixed || $right instanceof Type\Atomic\TMixed) {
1605
                return new Type\Atomic\TMixed;
1606
            }
1607
1608
            if ($c instanceof UnresolvedConstant\UnresolvedConcatOp) {
1609
                if ($left instanceof Type\Atomic\TLiteralString && $right instanceof Type\Atomic\TLiteralString) {
1610
                    return new Type\Atomic\TLiteralString($left->value . $right->value);
1611
                }
1612
1613
                return new Type\Atomic\TMixed;
1614
            }
1615
1616
            if ($c instanceof UnresolvedConstant\UnresolvedAdditionOp
1617
                || $c instanceof UnresolvedConstant\UnresolvedSubtractionOp
1618
                || $c instanceof UnresolvedConstant\UnresolvedDivisionOp
1619
                || $c instanceof UnresolvedConstant\UnresolvedMultiplicationOp
1620
                || $c instanceof UnresolvedConstant\UnresolvedBitwiseOr
1621
            ) {
1622
                if (($left instanceof Type\Atomic\TLiteralFloat || $left instanceof Type\Atomic\TLiteralInt)
1623
                    && ($right instanceof Type\Atomic\TLiteralFloat || $right instanceof Type\Atomic\TLiteralInt)
1624
                ) {
1625
                    if ($c instanceof UnresolvedConstant\UnresolvedAdditionOp) {
1626
                        return self::getLiteralTypeFromScalarValue($left->value + $right->value);
1627
                    }
1628
1629
                    if ($c instanceof UnresolvedConstant\UnresolvedSubtractionOp) {
1630
                        return self::getLiteralTypeFromScalarValue($left->value - $right->value);
1631
                    }
1632
1633
                    if ($c instanceof UnresolvedConstant\UnresolvedDivisionOp) {
1634
                        return self::getLiteralTypeFromScalarValue($left->value / $right->value);
1635
                    }
1636
1637
                    if ($c instanceof UnresolvedConstant\UnresolvedBitwiseOr) {
1638
                        return self::getLiteralTypeFromScalarValue($left->value | $right->value);
1639
                    }
1640
1641
                    return self::getLiteralTypeFromScalarValue($left->value * $right->value);
1642
                }
1643
1644
                return new Type\Atomic\TMixed;
1645
            }
1646
1647
            return new Type\Atomic\TMixed;
1648
        }
1649
1650
        if ($c instanceof UnresolvedConstant\UnresolvedTernary) {
1651
            $cond = $this->resolveConstantType(
1652
                $c->cond,
1653
                $statements_analyzer,
1654
                $visited_constant_ids + [$c_id => true]
1655
            );
1656
            $if = $c->if ? $this->resolveConstantType(
1657
                $c->if,
1658
                $statements_analyzer,
1659
                $visited_constant_ids + [$c_id => true]
1660
            ) : null;
1661
            $else = $this->resolveConstantType(
1662
                $c->else,
1663
                $statements_analyzer,
1664
                $visited_constant_ids + [$c_id => true]
1665
            );
1666
1667
            if ($cond instanceof Type\Atomic\TLiteralFloat
1668
                || $cond instanceof Type\Atomic\TLiteralInt
1669
                || $cond instanceof Type\Atomic\TLiteralString
1670
            ) {
1671
                if ($cond->value) {
1672
                    return $if ? $if : $cond;
1673
                }
1674
            } elseif ($cond instanceof Type\Atomic\TFalse || $cond instanceof Type\Atomic\TNull) {
1675
                return $else;
1676
            } elseif ($cond instanceof Type\Atomic\TTrue) {
1677
                return $if ? $if : $cond;
1678
            }
1679
        }
1680
1681
        if ($c instanceof UnresolvedConstant\ArrayValue) {
1682
            $properties = [];
1683
1684
            if (!$c->entries) {
1685
                return new Type\Atomic\TArray([Type::getEmpty(), Type::getEmpty()]);
1686
            }
1687
1688
            $is_list = true;
1689
1690
            foreach ($c->entries as $i => $entry) {
1691
                if ($entry->key) {
1692
                    $key_type = $this->resolveConstantType(
1693
                        $entry->key,
1694
                        $statements_analyzer,
1695
                        $visited_constant_ids + [$c_id => true]
1696
                    );
1697
1698
                    if (!$key_type instanceof Type\Atomic\TLiteralInt
1699
                        || $key_type->value !== $i
1700
                    ) {
1701
                        $is_list = false;
1702
                    }
1703
                } else {
1704
                    $key_type = new Type\Atomic\TLiteralInt($i);
1705
                }
1706
1707
                if ($key_type instanceof Type\Atomic\TLiteralInt
1708
                    || $key_type instanceof Type\Atomic\TLiteralString
1709
                ) {
1710
                    $key_value = $key_type->value;
1711
                } else {
1712
                    return new Type\Atomic\TArray([Type::getArrayKey(), Type::getMixed()]);
1713
                }
1714
1715
                $value_type = new Type\Union([$this->resolveConstantType(
1716
                    $entry->value,
1717
                    $statements_analyzer,
1718
                    $visited_constant_ids + [$c_id => true]
1719
                )]);
1720
1721
                $properties[$key_value] = $value_type;
1722
            }
1723
1724
            $objectlike = new Type\Atomic\ObjectLike($properties);
1725
1726
            $objectlike->is_list = $is_list;
1727
            $objectlike->sealed = true;
1728
1729
            return $objectlike;
1730
        }
1731
1732
        if ($c instanceof UnresolvedConstant\ClassConstant) {
1733
            if ($c->name === 'class') {
1734
                return new Type\Atomic\TLiteralClassString($c->fqcln);
1735
            }
1736
1737
            $found_type = $this->getConstantForClass(
1738
                $c->fqcln,
1739
                $c->name,
1740
                ReflectionProperty::IS_PRIVATE,
1741
                $statements_analyzer,
1742
                $visited_constant_ids + [$c_id => true]
1743
            );
1744
1745
            if ($found_type) {
1746
                return \array_values($found_type->getAtomicTypes())[0];
1747
            }
1748
        }
1749
1750
        if ($c instanceof UnresolvedConstant\ArrayOffsetFetch) {
1751
            $var_type = $this->resolveConstantType(
1752
                $c->array,
1753
                $statements_analyzer,
1754
                $visited_constant_ids + [$c_id => true]
1755
            );
1756
1757
            $offset_type = $this->resolveConstantType(
1758
                $c->offset,
1759
                $statements_analyzer,
1760
                $visited_constant_ids + [$c_id => true]
1761
            );
1762
1763
            if ($var_type instanceof Type\Atomic\ObjectLike
1764
                && ($offset_type instanceof Type\Atomic\TLiteralInt
1765
                    || $offset_type instanceof Type\Atomic\TLiteralString)
1766
            ) {
1767
                $union = $var_type->properties[$offset_type->value] ?? null;
1768
1769
                if ($union && $union->isSingle()) {
1770
                    return \array_values($union->getAtomicTypes())[0];
1771
                }
1772
            }
1773
        }
1774
1775
        if ($c instanceof UnresolvedConstant\Constant) {
1776
            if ($statements_analyzer) {
1777
                $found_type = ConstFetchAnalyzer::getConstType(
1778
                    $statements_analyzer,
1779
                    $c->name,
1780
                    $c->is_fully_qualified,
1781
                    null
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<Psalm\Context>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1782
                );
1783
1784
                if ($found_type) {
1785
                    return \array_values($found_type->getAtomicTypes())[0];
1786
                }
1787
            }
1788
        }
1789
1790
        return new Type\Atomic\TMixed;
1791
    }
1792
1793
    /**
1794
     * @param   string      $class_name
1795
     * @param   string      $const_name
1796
     * @param   Type\Union  $type
1797
     * @param   int         $visibility
1798
     *
1799
     * @return  void
1800
     */
1801
    public function setConstantType(
1802
        $class_name,
1803
        $const_name,
1804
        Type\Union $type,
1805
        $visibility
1806
    ) {
1807
        $storage = $this->classlike_storage_provider->get($class_name);
1808
1809
        if ($visibility === ReflectionProperty::IS_PUBLIC) {
1810
            $storage->public_class_constants[$const_name] = $type;
1811
        } elseif ($visibility === ReflectionProperty::IS_PROTECTED) {
1812
            $storage->protected_class_constants[$const_name] = $type;
1813
        } elseif ($visibility === ReflectionProperty::IS_PRIVATE) {
1814
            $storage->private_class_constants[$const_name] = $type;
1815
        }
1816
    }
1817
1818
    /**
1819
     * @return void
1820
     */
1821
    private function checkMethodReferences(ClassLikeStorage $classlike_storage, Methods $methods)
1822
    {
1823
        $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
1824
        $codebase = $project_analyzer->getCodebase();
1825
1826
        foreach ($classlike_storage->appearing_method_ids as $method_name => $appearing_method_id) {
1827
            $appearing_fq_classlike_name = $appearing_method_id->fq_class_name;
1828
1829
            if ($appearing_fq_classlike_name !== $classlike_storage->name) {
1830
                continue;
1831
            }
1832
1833
            $method_id = $appearing_method_id;
1834
1835
            $declaring_classlike_storage = $classlike_storage;
1836
1837
            if (isset($classlike_storage->methods[$method_name])) {
1838
                $method_storage = $classlike_storage->methods[$method_name];
1839
            } else {
1840
                $declaring_method_id = $classlike_storage->declaring_method_ids[$method_name];
1841
1842
                $declaring_fq_classlike_name = $declaring_method_id->fq_class_name;
1843
                $declaring_method_name = $declaring_method_id->method_name;
1844
1845
                try {
1846
                    $declaring_classlike_storage = $this->classlike_storage_provider->get($declaring_fq_classlike_name);
1847
                } catch (\InvalidArgumentException $e) {
1848
                    continue;
1849
                }
1850
1851
                $method_storage = $declaring_classlike_storage->methods[$declaring_method_name];
1852
                $method_id = $declaring_method_id;
1853
            }
1854
1855
            if ($method_storage->location
1856
                && !$project_analyzer->canReportIssues($method_storage->location->file_path)
1857
                && !$codebase->analyzer->canReportIssues($method_storage->location->file_path)
1858
            ) {
1859
                continue;
1860
            }
1861
1862
            $method_referenced = $this->file_reference_provider->isClassMethodReferenced(
1863
                strtolower((string) $method_id)
1864
            );
1865
1866
            if (!$method_referenced
1867
                && $method_name !== '__destruct'
1868
                && $method_name !== '__clone'
1869
                && $method_name !== '__invoke'
1870
                && $method_name !== '__unset'
1871
                && $method_name !== '__isset'
1872
                && $method_name !== '__sleep'
1873
                && $method_name !== '__wakeup'
1874
                && $method_name !== '__serialize'
1875
                && $method_name !== '__unserialize'
1876
                && $method_name !== '__set_state'
1877
                && $method_name !== '__debuginfo'
1878
                && $method_name !== '__tostring' // can be called in array_unique
1879
                && $method_storage->location
1880
            ) {
1881
                $method_location = $method_storage->location;
1882
1883
                $method_id = $classlike_storage->name . '::' . $method_storage->cased_name;
1884
1885
                if ($method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PRIVATE) {
1886
                    $has_parent_references = false;
1887
1888
                    if ($codebase->classImplements($classlike_storage->name, 'Serializable')
1889
                        && ($method_name === 'serialize' || $method_name === 'unserialize')
1890
                    ) {
1891
                        continue;
1892
                    }
1893
1894
                    $has_variable_calls = $codebase->analyzer->hasMixedMemberName($method_name)
1895
                        || $codebase->analyzer->hasMixedMemberName(strtolower($classlike_storage->name . '::'));
1896
1897
                    if (isset($classlike_storage->overridden_method_ids[$method_name])) {
1898
                        foreach ($classlike_storage->overridden_method_ids[$method_name] as $parent_method_id) {
1899
                            $parent_method_storage = $methods->getStorage($parent_method_id);
1900
1901
                            if ($parent_method_storage->location
1902
                                && !$project_analyzer->canReportIssues($parent_method_storage->location->file_path)
1903
                            ) {
1904
                                // here we just don’t know
1905
                                $has_parent_references = true;
1906
                                break;
1907
                            }
1908
1909
                            $parent_method_referenced = $this->file_reference_provider->isClassMethodReferenced(
1910
                                strtolower((string) $parent_method_id)
1911
                            );
1912
1913
                            if (!$parent_method_storage->abstract || $parent_method_referenced) {
1914
                                $has_parent_references = true;
1915
                                break;
1916
                            }
1917
                        }
1918
                    }
1919
1920
                    foreach ($classlike_storage->parent_classes as $parent_method_fqcln) {
1921
                        if ($codebase->analyzer->hasMixedMemberName(
1922
                            strtolower($parent_method_fqcln) . '::'
1923
                        )) {
1924
                            $has_variable_calls = true;
1925
                        }
1926
                    }
1927
1928
                    foreach ($classlike_storage->class_implements as $fq_interface_name_lc => $_) {
1929
                        try {
1930
                            $interface_storage = $this->classlike_storage_provider->get($fq_interface_name_lc);
1931
                        } catch (\InvalidArgumentException $e) {
1932
                            continue;
1933
                        }
1934
1935
                        if ($codebase->analyzer->hasMixedMemberName(
1936
                            $fq_interface_name_lc . '::'
1937
                        )) {
1938
                            $has_variable_calls = true;
1939
                        }
1940
1941
                        if (isset($interface_storage->methods[$method_name])) {
1942
                            $interface_method_referenced = $this->file_reference_provider->isClassMethodReferenced(
1943
                                $fq_interface_name_lc . '::' . $method_name
1944
                            );
1945
1946
                            if ($interface_method_referenced) {
1947
                                $has_parent_references = true;
1948
                            }
1949
                        }
1950
                    }
1951
1952
                    if (!$has_parent_references) {
1953
                        $issue = new PossiblyUnusedMethod(
1954
                            'Cannot find ' . ($has_variable_calls ? 'explicit' : 'any')
1955
                                . ' calls to method ' . $method_id
1956
                                . ($has_variable_calls ? ' (but did find some potential callers)' : ''),
1957
                            $method_storage->location,
1958
                            $method_id
1959
                        );
1960
1961
                        if ($codebase->alter_code) {
1962
                            if ($method_storage->stmt_location
1963
                                && !$declaring_classlike_storage->is_trait
1964
                                && isset($project_analyzer->getIssuesToFix()['PossiblyUnusedMethod'])
1965
                                && !$has_variable_calls
1966
                                && !IssueBuffer::isSuppressed($issue, $method_storage->suppressed_issues)
1967
                            ) {
1968
                                FileManipulationBuffer::addForCodeLocation(
1969
                                    $method_storage->stmt_location,
1970
                                    '',
1971
                                    true
1972
                                );
1973
                            }
1974
                        } elseif (IssueBuffer::accepts(
1975
                            $issue,
1976
                            $method_storage->suppressed_issues,
1977
                            $method_storage->stmt_location
1978
                                && !$declaring_classlike_storage->is_trait
1979
                                && !$has_variable_calls
1980
                        )) {
1981
                            // fall through
1982
                        }
1983
                    }
1984
                } elseif (!isset($classlike_storage->declaring_method_ids['__call'])) {
1985
                    $has_variable_calls = $codebase->analyzer->hasMixedMemberName(
1986
                        strtolower($classlike_storage->name . '::')
1987
                    ) || $codebase->analyzer->hasMixedMemberName($method_name);
1988
1989
                    $issue = new UnusedMethod(
1990
                        'Cannot find ' . ($has_variable_calls ? 'explicit' : 'any')
1991
                            . ' calls to private method ' . $method_id
1992
                            . ($has_variable_calls ? ' (but did find some potential callers)' : ''),
1993
                        $method_location,
1994
                        $method_id
1995
                    );
1996
1997
                    if ($codebase->alter_code) {
1998
                        if ($method_storage->stmt_location
1999
                            && !$declaring_classlike_storage->is_trait
2000
                            && isset($project_analyzer->getIssuesToFix()['UnusedMethod'])
2001
                            && !$has_variable_calls
2002
                            && !IssueBuffer::isSuppressed($issue, $method_storage->suppressed_issues)
2003
                        ) {
2004
                            FileManipulationBuffer::addForCodeLocation(
2005
                                $method_storage->stmt_location,
2006
                                '',
2007
                                true
2008
                            );
2009
                        }
2010
                    } elseif (IssueBuffer::accepts(
2011
                        $issue,
2012
                        $method_storage->suppressed_issues,
2013
                        $method_storage->stmt_location
2014
                            && !$declaring_classlike_storage->is_trait
2015
                            && !$has_variable_calls
2016
                    )) {
2017
                        // fall through
2018
                    }
2019
                }
2020
            } else {
2021
                if ($method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PRIVATE
2022
                    && !$classlike_storage->is_interface
2023
                ) {
2024
                    foreach ($method_storage->params as $offset => $param_storage) {
2025
                        if (!$this->file_reference_provider->isMethodParamUsed(
2026
                            strtolower((string) $method_id),
2027
                            $offset
2028
                        )
2029
                            && $param_storage->location
2030
                        ) {
2031
                            if ($method_storage->final) {
2032
                                if (IssueBuffer::accepts(
2033
                                    new \Psalm\Issue\UnusedParam(
2034
                                        'Param #' . ($offset + 1) . ' is never referenced in this method',
2035
                                        $param_storage->location
2036
                                    ),
2037
                                    $method_storage->suppressed_issues
2038
                                )) {
2039
                                    // fall through
2040
                                }
2041
                            } else {
2042
                                if (IssueBuffer::accepts(
2043
                                    new PossiblyUnusedParam(
2044
                                        'Param #' . ($offset + 1) . ' is never referenced in this method',
2045
                                        $param_storage->location
2046
                                    ),
2047
                                    $method_storage->suppressed_issues
2048
                                )) {
2049
                                    // fall through
2050
                                }
2051
                            }
2052
                        }
2053
                    }
2054
                }
2055
            }
2056
        }
2057
    }
2058
2059
    /**
2060
     * @return void
2061
     */
2062
    private function findPossibleMethodParamTypes(ClassLikeStorage $classlike_storage)
2063
    {
2064
        $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
2065
        $codebase = $project_analyzer->getCodebase();
2066
2067
        foreach ($classlike_storage->appearing_method_ids as $method_name => $appearing_method_id) {
2068
            $appearing_fq_classlike_name = $appearing_method_id->fq_class_name;
2069
2070
            if ($appearing_fq_classlike_name !== $classlike_storage->name) {
2071
                continue;
2072
            }
2073
2074
            $method_id = $appearing_method_id;
2075
2076
            $declaring_classlike_storage = $classlike_storage;
2077
2078
            if (isset($classlike_storage->methods[$method_name])) {
2079
                $method_storage = $classlike_storage->methods[$method_name];
2080
            } else {
2081
                $declaring_method_id = $classlike_storage->declaring_method_ids[$method_name];
2082
2083
                $declaring_fq_classlike_name = $declaring_method_id->fq_class_name;
2084
                $declaring_method_name = $declaring_method_id->method_name;
2085
2086
                try {
2087
                    $declaring_classlike_storage = $this->classlike_storage_provider->get($declaring_fq_classlike_name);
2088
                } catch (\InvalidArgumentException $e) {
2089
                    continue;
2090
                }
2091
2092
                $method_storage = $declaring_classlike_storage->methods[$declaring_method_name];
2093
                $method_id = $declaring_method_id;
2094
            }
2095
2096
            if ($method_storage->location
2097
                && !$project_analyzer->canReportIssues($method_storage->location->file_path)
2098
                && !$codebase->analyzer->canReportIssues($method_storage->location->file_path)
2099
            ) {
2100
                continue;
2101
            }
2102
2103
            if ($declaring_classlike_storage->is_trait) {
2104
                continue;
2105
            }
2106
2107
            $method_id_lc = strtolower((string) $method_id);
2108
2109
            if (isset($codebase->analyzer->possible_method_param_types[$method_id_lc])) {
2110
                if ($method_storage->location) {
2111
                    $possible_param_types
2112
                        = $codebase->analyzer->possible_method_param_types[$method_id_lc];
2113
2114
                    if ($possible_param_types) {
2115
                        foreach ($possible_param_types as $offset => $possible_type) {
2116
                            if (!isset($method_storage->params[$offset])) {
2117
                                continue;
2118
                            }
2119
2120
                            $param_name = $method_storage->params[$offset]->name;
2121
2122
                            if ($possible_type->hasMixed() || $possible_type->isNull()) {
2123
                                continue;
2124
                            }
2125
2126
                            if ($method_storage->params[$offset]->default_type) {
2127
                                $possible_type = \Psalm\Type::combineUnionTypes(
2128
                                    $possible_type,
2129
                                    $method_storage->params[$offset]->default_type
2130
                                );
2131
                            }
2132
2133
                            if ($codebase->alter_code
2134
                                && isset($project_analyzer->getIssuesToFix()['MissingParamType'])
2135
                            ) {
2136
                                $function_analyzer = $project_analyzer->getFunctionLikeAnalyzer(
2137
                                    $method_id,
2138
                                    $method_storage->location->file_path
2139
                                );
2140
2141
                                $has_variable_calls = $codebase->analyzer->hasMixedMemberName(
2142
                                    $method_name
2143
                                )
2144
                                    || $codebase->analyzer->hasMixedMemberName(
2145
                                        strtolower($classlike_storage->name . '::')
2146
                                    );
2147
2148
                                if ($has_variable_calls) {
2149
                                    $possible_type->from_docblock = true;
2150
                                }
2151
2152
                                if ($function_analyzer) {
2153
                                    $function_analyzer->addOrUpdateParamType(
2154
                                        $project_analyzer,
2155
                                        $param_name,
2156
                                        $possible_type,
2157
                                        $possible_type->from_docblock
2158
                                            && $project_analyzer->only_replace_php_types_with_non_docblock_types
2159
                                    );
2160
                                }
2161
                            } else {
2162
                                IssueBuffer::addFixableIssue('MissingParamType');
2163
                            }
2164
                        }
2165
                    }
2166
                }
2167
            }
2168
        }
2169
    }
2170
2171
    /**
2172
     * @return void
2173
     */
2174
    private function checkPropertyReferences(ClassLikeStorage $classlike_storage)
2175
    {
2176
        $project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
2177
        $codebase = $project_analyzer->getCodebase();
2178
2179
        foreach ($classlike_storage->properties as $property_name => $property_storage) {
2180
            $referenced_property_name = strtolower($classlike_storage->name) . '::$' . $property_name;
2181
            $property_referenced = $this->file_reference_provider->isClassPropertyReferenced(
2182
                $referenced_property_name
2183
            );
2184
2185
            $property_constructor_referenced = false;
2186
            if ($property_referenced && $property_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE) {
2187
                $all_method_references = $this->file_reference_provider->getAllMethodReferencesToClassMembers();
2188
2189
                if (isset($all_method_references[$referenced_property_name])
2190
                    && count($all_method_references[$referenced_property_name]) === 1) {
2191
                    $constructor_name = strtolower($classlike_storage->name) . '::__construct';
2192
                    $property_references = $all_method_references[$referenced_property_name];
2193
2194
                    $property_constructor_referenced = isset($property_references[$constructor_name])
2195
                        && !$property_storage->is_static;
2196
                }
2197
            }
2198
2199
            if ((!$property_referenced || $property_constructor_referenced)
2200
                && $property_storage->location
2201
            ) {
2202
                $property_id = $classlike_storage->name . '::$' . $property_name;
2203
2204
                if ($property_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC
2205
                    || $property_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PROTECTED
2206
                ) {
2207
                    $has_parent_references = isset($classlike_storage->overridden_property_ids[$property_name]);
2208
2209
                    $has_variable_calls = $codebase->analyzer->hasMixedMemberName('$' . $property_name)
2210
                        || $codebase->analyzer->hasMixedMemberName(strtolower($classlike_storage->name) . '::$');
2211
2212
                    foreach ($classlike_storage->parent_classes as $parent_method_fqcln) {
2213
                        if ($codebase->analyzer->hasMixedMemberName(
2214
                            strtolower($parent_method_fqcln) . '::$'
2215
                        )) {
2216
                            $has_variable_calls = true;
2217
                            break;
2218
                        }
2219
                    }
2220
2221
                    foreach ($classlike_storage->class_implements as $fq_interface_name) {
2222
                        if ($codebase->analyzer->hasMixedMemberName(
2223
                            strtolower($fq_interface_name) . '::$'
2224
                        )) {
2225
                            $has_variable_calls = true;
2226
                            break;
2227
                        }
2228
                    }
2229
2230
                    if (!$has_parent_references
2231
                        && ($property_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC
2232
                            || !isset($classlike_storage->declaring_method_ids['__get']))
2233
                    ) {
2234
                        $issue = new PossiblyUnusedProperty(
2235
                            'Cannot find ' . ($has_variable_calls ? 'explicit' : 'any')
2236
                                . ' references to property ' . $property_id
2237
                                . ($has_variable_calls ? ' (but did find some potential references)' : ''),
2238
                            $property_storage->location
2239
                        );
2240
2241
                        if ($codebase->alter_code) {
2242
                            if ($property_storage->stmt_location
2243
                                && isset($project_analyzer->getIssuesToFix()['PossiblyUnusedProperty'])
2244
                                && !$has_variable_calls
2245
                                && !IssueBuffer::isSuppressed($issue, $classlike_storage->suppressed_issues)
2246
                            ) {
2247
                                FileManipulationBuffer::addForCodeLocation(
2248
                                    $property_storage->stmt_location,
2249
                                    '',
2250
                                    true
2251
                                );
2252
                            }
2253
                        } elseif (IssueBuffer::accepts(
2254
                            $issue,
2255
                            $classlike_storage->suppressed_issues
2256
                        )) {
2257
                            // fall through
2258
                        }
2259
                    }
2260
                } elseif (!isset($classlike_storage->declaring_method_ids['__get'])) {
2261
                    $has_variable_calls = $codebase->analyzer->hasMixedMemberName('$' . $property_name);
2262
2263
                    $issue = new UnusedProperty(
2264
                        'Cannot find ' . ($has_variable_calls ? 'explicit' : 'any')
2265
                            . ' references to private property ' . $property_id
2266
                            . ($has_variable_calls ? ' (but did find some potential references)' : ''),
2267
                        $property_storage->location
2268
                    );
2269
2270
                    if ($codebase->alter_code) {
2271
                        if (!$property_constructor_referenced
2272
                            && $property_storage->stmt_location
2273
                            && isset($project_analyzer->getIssuesToFix()['UnusedProperty'])
2274
                            && !$has_variable_calls
2275
                            && !IssueBuffer::isSuppressed($issue, $classlike_storage->suppressed_issues)
2276
                        ) {
2277
                            FileManipulationBuffer::addForCodeLocation(
2278
                                $property_storage->stmt_location,
2279
                                '',
2280
                                true
2281
                            );
2282
                        }
2283
                    } elseif (IssueBuffer::accepts(
2284
                        $issue,
2285
                        $classlike_storage->suppressed_issues
2286
                    )) {
2287
                        // fall through
2288
                    }
2289
                }
2290
            }
2291
        }
2292
    }
2293
2294
    /**
2295
     * @param  lowercase-string $fq_classlike_name_lc
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2296
     *
2297
     * @return void
2298
     */
2299
    public function registerMissingClassLike($fq_classlike_name_lc)
2300
    {
2301
        $this->existing_classlikes_lc[$fq_classlike_name_lc] = false;
2302
    }
2303
2304
    /**
2305
     * @param  lowercase-string $fq_classlike_name_lc
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2306
     *
2307
     * @return bool
2308
     */
2309
    public function isMissingClassLike($fq_classlike_name_lc)
2310
    {
2311
        return isset($this->existing_classlikes_lc[$fq_classlike_name_lc])
2312
            && $this->existing_classlikes_lc[$fq_classlike_name_lc] === false;
2313
    }
2314
2315
    /**
2316
     * @param  lowercase-string $fq_classlike_name_lc
0 ignored issues
show
Documentation introduced by
The doc-type lowercase-string could not be parsed: Unknown type name "lowercase-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2317
     *
2318
     * @return bool
2319
     */
2320
    public function doesClassLikeExist($fq_classlike_name_lc)
2321
    {
2322
        return isset($this->existing_classlikes_lc[$fq_classlike_name_lc])
2323
            && $this->existing_classlikes_lc[$fq_classlike_name_lc];
2324
    }
2325
2326
    public function forgetMissingClassLikes() : void
2327
    {
2328
        $this->existing_classlikes_lc = \array_filter($this->existing_classlikes_lc);
2329
    }
2330
2331
    /**
2332
     * @param string $fq_class_name
2333
     *
2334
     * @return void
2335
     */
2336
    public function removeClassLike($fq_class_name)
2337
    {
2338
        $fq_class_name_lc = strtolower($fq_class_name);
2339
2340
        unset(
2341
            $this->existing_classlikes_lc[$fq_class_name_lc],
2342
            $this->existing_classes_lc[$fq_class_name_lc],
2343
            $this->existing_traits_lc[$fq_class_name_lc],
2344
            $this->existing_traits[$fq_class_name],
2345
            $this->existing_interfaces_lc[$fq_class_name_lc],
2346
            $this->existing_interfaces[$fq_class_name],
2347
            $this->existing_classes[$fq_class_name],
2348
            $this->trait_nodes[$fq_class_name_lc]
2349
        );
2350
2351
        $this->scanner->removeClassLike($fq_class_name_lc);
2352
    }
2353
2354
    /**
2355
     * @return array{
0 ignored issues
show
Documentation introduced by
The doc-type array{ could not be parsed: Unknown type name "array{" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2356
     *     0: array<lowercase-string, bool>,
2357
     *     1: array<lowercase-string, bool>,
2358
     *     2: array<lowercase-string, bool>,
2359
     *     3: array<string, bool>,
2360
     *     4: array<lowercase-string, bool>,
2361
     *     5: array<string, bool>,
2362
     *     6: array<string, bool>,
2363
     * }
2364
     */
2365
    public function getThreadData()
2366
    {
2367
        return [
2368
            $this->existing_classlikes_lc,
2369
            $this->existing_classes_lc,
2370
            $this->existing_traits_lc,
2371
            $this->existing_traits,
2372
            $this->existing_interfaces_lc,
2373
            $this->existing_interfaces,
2374
            $this->existing_classes,
2375
        ];
2376
    }
2377
2378
    /**
2379
     * @param array{
2380
     *     0: array<lowercase-string, bool>,
2381
     *     1: array<lowercase-string, bool>,
2382
     *     2: array<lowercase-string, bool>,
2383
     *     3: array<string, bool>,
2384
     *     4: array<lowercase-string, bool>,
2385
     *     5: array<string, bool>,
2386
     *     6: array<string, bool>,
2387
     * } $thread_data
2388
     *
2389
     * @return void
2390
     */
2391
    public function addThreadData(array $thread_data)
2392
    {
2393
        list(
2394
            $existing_classlikes_lc,
2395
            $existing_classes_lc,
2396
            $existing_traits_lc,
2397
            $existing_traits,
2398
            $existing_interfaces_lc,
2399
            $existing_interfaces,
2400
            $existing_classes) = $thread_data;
2401
2402
        $this->existing_classlikes_lc = array_merge($existing_classlikes_lc, $this->existing_classlikes_lc);
2403
        $this->existing_classes_lc = array_merge($existing_classes_lc, $this->existing_classes_lc);
2404
        $this->existing_traits_lc = array_merge($existing_traits_lc, $this->existing_traits_lc);
2405
        $this->existing_traits = array_merge($existing_traits, $this->existing_traits);
2406
        $this->existing_interfaces_lc = array_merge($existing_interfaces_lc, $this->existing_interfaces_lc);
2407
        $this->existing_interfaces = array_merge($existing_interfaces, $this->existing_interfaces);
2408
        $this->existing_classes = array_merge($existing_classes, $this->existing_classes);
2409
    }
2410
}
2411