Codebase::getSymbolInformation()   C
last analyzed

Complexity

Conditions 12
Paths 32

Size

Total Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
nc 32
nop 2
dl 0
loc 73
rs 6.1624
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
namespace Psalm;
3
4
use Psalm\Internal\Analyzer\StatementsAnalyzer;
5
use function array_combine;
6
use function array_merge;
7
use function count;
8
use function error_log;
9
use function explode;
10
use function in_array;
11
use function krsort;
12
use function ksort;
13
use LanguageServerProtocol\Command;
14
use LanguageServerProtocol\Position;
15
use LanguageServerProtocol\Range;
16
use const PHP_MAJOR_VERSION;
17
use const PHP_MINOR_VERSION;
18
use PhpParser;
19
use function preg_match;
20
use Psalm\Internal\Analyzer\ProjectAnalyzer;
21
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
22
use Psalm\Internal\Analyzer\TypeAnalyzer;
23
use Psalm\Internal\Codebase\InternalCallMapHandler;
24
use Psalm\Internal\Provider\ClassLikeStorageProvider;
25
use Psalm\Internal\Provider\FileProvider;
26
use Psalm\Internal\Provider\FileReferenceProvider;
27
use Psalm\Internal\Provider\FileStorageProvider;
28
use Psalm\Internal\Provider\Providers;
29
use Psalm\Internal\Provider\StatementsProvider;
30
use Psalm\Progress\Progress;
31
use Psalm\Progress\VoidProgress;
32
use Psalm\Storage\ClassLikeStorage;
33
use Psalm\Storage\FileStorage;
34
use Psalm\Storage\FunctionLikeStorage;
35
use function is_string;
36
use function strlen;
37
use function strpos;
38
use function strrpos;
39
use function strtolower;
40
use function substr;
41
use function substr_count;
42
43
class Codebase
44
{
45
    /**
46
     * @var Config
47
     */
48
    public $config;
49
50
    /**
51
     * A map of fully-qualified use declarations to the files
52
     * that reference them (keyed by filename)
53
     *
54
     * @var array<string, array<int, \Psalm\CodeLocation>>
55
     */
56
    public $use_referencing_locations = [];
57
58
    /**
59
     * A map of file names to the classes that they contain explicit references to
60
     * used in collaboration with use_referencing_locations
61
     *
62
     * @var array<string, array<string, bool>>
63
     */
64
    public $use_referencing_files = [];
65
66
    /**
67
     * @var FileStorageProvider
68
     */
69
    public $file_storage_provider;
70
71
    /**
72
     * @var ClassLikeStorageProvider
73
     */
74
    public $classlike_storage_provider;
75
76
    /**
77
     * @var bool
78
     */
79
    public $collect_references = false;
80
81
    /**
82
     * @var bool
83
     */
84
    public $collect_locations = false;
85
86
    /**
87
     * @var null|'always'|'auto'
88
     */
89
    public $find_unused_code = null;
90
91
    /**
92
     * @var FileProvider
93
     */
94
    public $file_provider;
95
96
    /**
97
     * @var FileReferenceProvider
98
     */
99
    public $file_reference_provider;
100
101
    /**
102
     * @var StatementsProvider
103
     */
104
    public $statements_provider;
105
106
    /**
107
     * @var Progress
108
     */
109
    private $progress;
110
111
    /**
112
     * @var array<string, Type\Union>
113
     */
114
    private static $stubbed_constants = [];
115
116
    /**
117
     * Whether to register autoloaded information
118
     *
119
     * @var bool
120
     */
121
    public $register_autoload_files = false;
122
123
    /**
124
     * Whether to log functions just at the file level or globally (for stubs)
125
     *
126
     * @var bool
127
     */
128
    public $register_stub_files = false;
129
130
    /**
131
     * @var bool
132
     */
133
    public $find_unused_variables = false;
134
135
    /**
136
     * @var Internal\Codebase\Scanner
137
     */
138
    public $scanner;
139
140
    /**
141
     * @var Internal\Codebase\Analyzer
142
     */
143
    public $analyzer;
144
145
    /**
146
     * @var Internal\Codebase\Functions
147
     */
148
    public $functions;
149
150
    /**
151
     * @var Internal\Codebase\ClassLikes
152
     */
153
    public $classlikes;
154
155
    /**
156
     * @var Internal\Codebase\Methods
157
     */
158
    public $methods;
159
160
    /**
161
     * @var Internal\Codebase\Properties
162
     */
163
    public $properties;
164
165
    /**
166
     * @var Internal\Codebase\Populator
167
     */
168
    public $populator;
169
170
    /**
171
     * @var ?Internal\Codebase\Taint
172
     */
173
    public $taint = null;
174
175
    /**
176
     * @var bool
177
     */
178
    public $server_mode = false;
179
180
    /**
181
     * @var bool
182
     */
183
    public $store_node_types = false;
184
185
    /**
186
     * Whether or not to infer types from usage. Computationally expensive, so turned off by default
187
     *
188
     * @var bool
189
     */
190
    public $infer_types_from_usage = false;
191
192
    /**
193
     * @var bool
194
     */
195
    public $alter_code = false;
196
197
    /**
198
     * @var bool
199
     */
200
    public $diff_methods = false;
201
202
    /**
203
     * @var array<lowercase-string, string>
204
     */
205
    public $methods_to_move = [];
206
207
    /**
208
     * @var array<lowercase-string, string>
209
     */
210
    public $methods_to_rename = [];
211
212
    /**
213
     * @var array<string, string>
214
     */
215
    public $properties_to_move = [];
216
217
    /**
218
     * @var array<string, string>
219
     */
220
    public $properties_to_rename = [];
221
222
    /**
223
     * @var array<string, string>
224
     */
225
    public $class_constants_to_move = [];
226
227
    /**
228
     * @var array<string, string>
229
     */
230
    public $class_constants_to_rename = [];
231
232
    /**
233
     * @var array<lowercase-string, string>
234
     */
235
    public $classes_to_move = [];
236
237
    /**
238
     * @var array<string, string>
239
     */
240
    public $call_transforms = [];
241
242
    /**
243
     * @var array<string, string>
244
     */
245
    public $property_transforms = [];
246
247
    /**
248
     * @var array<string, string>
249
     */
250
    public $class_constant_transforms = [];
251
252
    /**
253
     * @var array<lowercase-string, string>
254
     */
255
    public $class_transforms = [];
256
257
    /**
258
     * @var bool
259
     */
260
    public $allow_backwards_incompatible_changes = true;
261
262
    /**
263
     * @var int
264
     */
265
    public $php_major_version = PHP_MAJOR_VERSION;
266
267
    /**
268
     * @var int
269
     */
270
    public $php_minor_version = PHP_MINOR_VERSION;
271
272
    /**
273
     * @var bool
274
     */
275
    public $track_unused_suppressions = false;
276
277
    public function __construct(
278
        Config $config,
279
        Providers $providers,
280
        Progress $progress = null
281
    ) {
282
        if ($progress === null) {
283
            $progress = new VoidProgress();
284
        }
285
286
        $this->config = $config;
287
        $this->file_storage_provider = $providers->file_storage_provider;
288
        $this->classlike_storage_provider = $providers->classlike_storage_provider;
289
        $this->progress = $progress;
290
        $this->file_provider = $providers->file_provider;
291
        $this->file_reference_provider = $providers->file_reference_provider;
292
        $this->statements_provider = $providers->statements_provider;
293
294
        self::$stubbed_constants = [];
295
296
        $reflection = new Internal\Codebase\Reflection($providers->classlike_storage_provider, $this);
297
298
        $this->scanner = new Internal\Codebase\Scanner(
299
            $this,
300
            $config,
301
            $providers->file_storage_provider,
302
            $providers->file_provider,
303
            $reflection,
304
            $providers->file_reference_provider,
305
            $progress
306
        );
307
308
        $this->loadAnalyzer();
309
310
        $this->functions = new Internal\Codebase\Functions($providers->file_storage_provider, $reflection);
311
312
        $this->properties = new Internal\Codebase\Properties(
313
            $providers->classlike_storage_provider,
314
            $providers->file_reference_provider
315
        );
316
317
        $this->classlikes = new Internal\Codebase\ClassLikes(
318
            $this->config,
319
            $providers->classlike_storage_provider,
320
            $providers->file_reference_provider,
321
            $providers->statements_provider,
322
            $this->scanner
323
        );
324
325
        $this->methods = new Internal\Codebase\Methods(
326
            $providers->classlike_storage_provider,
327
            $providers->file_reference_provider,
328
            $this->classlikes
329
        );
330
331
        $this->populator = new Internal\Codebase\Populator(
332
            $config,
333
            $providers->classlike_storage_provider,
334
            $providers->file_storage_provider,
335
            $this->classlikes,
336
            $providers->file_reference_provider,
337
            $progress
338
        );
339
340
        $this->loadAnalyzer();
341
    }
342
343
    /**
344
     * @return void
345
     */
346
    private function loadAnalyzer()
347
    {
348
        $this->analyzer = new Internal\Codebase\Analyzer(
349
            $this->config,
350
            $this->file_provider,
351
            $this->file_storage_provider,
352
            $this->progress
353
        );
354
    }
355
356
    /**
357
     * @param array<string> $candidate_files
358
     *
359
     * @return void
360
     */
361
    public function reloadFiles(ProjectAnalyzer $project_analyzer, array $candidate_files)
362
    {
363
        $this->loadAnalyzer();
364
365
        $this->file_reference_provider->loadReferenceCache(false);
366
367
        Internal\Analyzer\FunctionLikeAnalyzer::clearCache();
368
369
        if (!$this->statements_provider->parser_cache_provider) {
370
            $diff_files = $candidate_files;
371
        } else {
372
            $diff_files = [];
373
374
            $parser_cache_provider = $this->statements_provider->parser_cache_provider;
375
376
            foreach ($candidate_files as $candidate_file_path) {
377
                if ($parser_cache_provider->loadExistingFileContentsFromCache($candidate_file_path)
378
                    !== $this->file_provider->getContents($candidate_file_path)
379
                ) {
380
                    $diff_files[] = $candidate_file_path;
381
                }
382
            }
383
        }
384
385
        $referenced_files = $project_analyzer->getReferencedFilesFromDiff($diff_files, false);
386
387
        foreach ($diff_files as $diff_file_path) {
388
            $this->invalidateInformationForFile($diff_file_path);
389
        }
390
391
        foreach ($referenced_files as $referenced_file_path) {
392
            if (in_array($referenced_file_path, $diff_files, true)) {
393
                continue;
394
            }
395
396
            $file_storage = $this->file_storage_provider->get($referenced_file_path);
397
398
            foreach ($file_storage->classlikes_in_file as $fq_classlike_name) {
399
                $this->classlike_storage_provider->remove($fq_classlike_name);
400
                $this->classlikes->removeClassLike($fq_classlike_name);
401
            }
402
403
            $this->file_storage_provider->remove($referenced_file_path);
404
            $this->scanner->removeFile($referenced_file_path);
405
        }
406
407
        $referenced_files = array_combine($referenced_files, $referenced_files);
408
409
        $this->scanner->addFilesToDeepScan($referenced_files);
410
        $this->addFilesToAnalyze(array_combine($candidate_files, $candidate_files));
411
412
        $this->scanner->scanFiles($this->classlikes);
413
414
        $this->file_reference_provider->updateReferenceCache($this, $referenced_files);
415
416
        $this->populator->populateCodebase();
417
    }
418
419
    /** @return void */
420
    public function enterServerMode()
421
    {
422
        $this->server_mode = true;
423
        $this->store_node_types = true;
424
    }
425
426
    /**
427
     * @return void
428
     */
429
    public function collectLocations()
430
    {
431
        $this->collect_locations = true;
432
        $this->classlikes->collect_locations = true;
433
        $this->methods->collect_locations = true;
434
        $this->properties->collect_locations = true;
435
    }
436
437
    /**
438
     * @param 'always'|'auto' $find_unused_code
0 ignored issues
show
Documentation introduced by
The doc-type 'always'|'auto' could not be parsed: Unknown type name "'always'" 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...
439
     *
440
     * @return void
441
     */
442
    public function reportUnusedCode(string $find_unused_code = 'auto')
443
    {
444
        $this->collect_references = true;
445
        $this->classlikes->collect_references = true;
446
        $this->find_unused_code = $find_unused_code;
447
        $this->find_unused_variables = true;
448
    }
449
450
    /**
451
     * @return void
452
     */
453
    public function reportUnusedVariables()
454
    {
455
        $this->collect_references = true;
456
        $this->find_unused_variables = true;
457
    }
458
459
    /**
460
     * @param array<string, string> $files_to_analyze
461
     *
462
     * @return void
463
     */
464
    public function addFilesToAnalyze(array $files_to_analyze)
465
    {
466
        $this->scanner->addFilesToDeepScan($files_to_analyze);
467
        $this->analyzer->addFilesToAnalyze($files_to_analyze);
468
    }
469
470
    /**
471
     * Scans all files their related files
472
     *
473
     * @return void
474
     */
475
    public function scanFiles(int $threads = 1)
476
    {
477
        $has_changes = $this->scanner->scanFiles($this->classlikes, $threads);
478
479
        if ($has_changes) {
480
            $this->populator->populateCodebase();
481
        }
482
    }
483
484
    /**
485
     * @param  string $file_path
486
     *
487
     * @return string
488
     */
489
    public function getFileContents($file_path)
490
    {
491
        return $this->file_provider->getContents($file_path);
492
    }
493
494
    /**
495
     * @param  string $file_path
496
     *
497
     * @return list<PhpParser\Node\Stmt>
0 ignored issues
show
Documentation introduced by
The doc-type list<PhpParser\Node\Stmt> could not be parsed: Expected "|" or "end of type", but got "<" at position 4. (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...
498
     */
499
    public function getStatementsForFile($file_path)
500
    {
501
        return $this->statements_provider->getStatementsForFile(
502
            $file_path,
503
            $this->progress
504
        );
505
    }
506
507
    /**
508
     * @param  string $fq_classlike_name
509
     *
510
     * @return ClassLikeStorage
511
     */
512
    public function createClassLikeStorage($fq_classlike_name)
513
    {
514
        return $this->classlike_storage_provider->create($fq_classlike_name);
515
    }
516
517
    /**
518
     * @param  string $file_path
519
     *
520
     * @return void
521
     */
522
    public function cacheClassLikeStorage(ClassLikeStorage $classlike_storage, $file_path)
523
    {
524
        $file_contents = $this->file_provider->getContents($file_path);
525
526
        if ($this->classlike_storage_provider->cache) {
527
            $this->classlike_storage_provider->cache->writeToCache($classlike_storage, $file_path, $file_contents);
528
        }
529
    }
530
531
    /**
532
     * @param  string $fq_classlike_name
533
     * @param  string $file_path
534
     *
535
     * @return void
536
     */
537
    public function exhumeClassLikeStorage($fq_classlike_name, $file_path)
538
    {
539
        $file_contents = $this->file_provider->getContents($file_path);
540
        $storage = $this->classlike_storage_provider->exhume(
541
            $fq_classlike_name,
542
            $file_path,
543
            $file_contents
544
        );
545
546
        if ($storage->is_trait) {
547
            $this->classlikes->addFullyQualifiedTraitName($storage->name, $file_path);
548
        } elseif ($storage->is_interface) {
549
            $this->classlikes->addFullyQualifiedInterfaceName($storage->name, $file_path);
550
        } else {
551
            $this->classlikes->addFullyQualifiedClassName($storage->name, $file_path);
552
        }
553
    }
554
555
    /**
556
     * @param  ?\ReflectionType $type
0 ignored issues
show
Documentation introduced by
The doc-type ?\ReflectionType could not be parsed: Unknown type name "?\ReflectionType" 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...
557
     */
558
    public static function getPsalmTypeFromReflection($type) : Type\Union
559
    {
560
        return \Psalm\Internal\Codebase\Reflection::getPsalmTypeFromReflectionType($type);
561
    }
562
563
    /**
564
     * @param  string $file_path
565
     *
566
     * @return FileStorage
567
     */
568
    public function createFileStorageForPath($file_path)
569
    {
570
        return $this->file_storage_provider->create($file_path);
571
    }
572
573
    /**
574
     * @param  string $symbol
575
     *
576
     * @return \Psalm\CodeLocation[]
577
     */
578
    public function findReferencesToSymbol($symbol)
579
    {
580
        if (!$this->collect_locations) {
581
            throw new \UnexpectedValueException('Should not be checking references');
582
        }
583
584
        if (strpos($symbol, '::$') !== false) {
585
            return $this->findReferencesToProperty($symbol);
586
        }
587
588
        if (strpos($symbol, '::') !== false) {
589
            return $this->findReferencesToMethod($symbol);
590
        }
591
592
        return $this->findReferencesToClassLike($symbol);
593
    }
594
595
    /**
596
     * @param  string $method_id
597
     *
598
     * @return \Psalm\CodeLocation[]
599
     */
600
    public function findReferencesToMethod($method_id)
601
    {
602
        return $this->file_reference_provider->getClassMethodLocations(strtolower($method_id));
603
    }
604
605
    /**
606
     * @return \Psalm\CodeLocation[]
607
     */
608
    public function findReferencesToProperty(string $property_id)
609
    {
610
        list($fq_class_name, $property_name) = explode('::', $property_id);
611
612
        return $this->file_reference_provider->getClassPropertyLocations(
613
            strtolower($fq_class_name) . '::' . $property_name
614
        );
615
    }
616
617
    /**
618
     * @param  string $fq_class_name
619
     *
620
     * @return \Psalm\CodeLocation[]
621
     */
622
    public function findReferencesToClassLike($fq_class_name)
623
    {
624
        $fq_class_name_lc = strtolower($fq_class_name);
625
        $locations = $this->file_reference_provider->getClassLocations($fq_class_name_lc);
626
627
        if (isset($this->use_referencing_locations[$fq_class_name_lc])) {
628
            $locations = array_merge($locations, $this->use_referencing_locations[$fq_class_name_lc]);
629
        }
630
631
        return $locations;
632
    }
633
634
    /**
635
     * @param  string $file_path
636
     * @param  string $closure_id
637
     *
638
     * @return FunctionLikeStorage
639
     */
640
    public function getClosureStorage($file_path, $closure_id)
641
    {
642
        $file_storage = $this->file_storage_provider->get($file_path);
643
644
        // closures can be returned here
645
        if (isset($file_storage->functions[$closure_id])) {
646
            return $file_storage->functions[$closure_id];
647
        }
648
649
        throw new \UnexpectedValueException(
650
            'Expecting ' . $closure_id . ' to have storage in ' . $file_path
651
        );
652
    }
653
654
    /**
655
     * @param  string $const_id
656
     * @param  Type\Union $type
657
     *
658
     * @return  void
659
     */
660
    public function addGlobalConstantType($const_id, Type\Union $type)
661
    {
662
        self::$stubbed_constants[$const_id] = $type;
663
    }
664
665
    /**
666
     * @param  string $const_id
667
     *
668
     * @return Type\Union|null
669
     */
670
    public function getStubbedConstantType($const_id)
671
    {
672
        return isset(self::$stubbed_constants[$const_id]) ? self::$stubbed_constants[$const_id] : null;
673
    }
674
675
    /**
676
     * @return array<string, Type\Union>
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...
677
     */
678
    public function getAllStubbedConstants()
679
    {
680
        return self::$stubbed_constants;
681
    }
682
683
    /**
684
     * @param  string $file_path
685
     *
686
     * @return bool
687
     */
688
    public function fileExists($file_path)
689
    {
690
        return $this->file_provider->fileExists($file_path);
691
    }
692
693
    /**
694
     * Check whether a class/interface exists
695
     *
696
     * @param  string          $fq_class_name
697
     * @param  CodeLocation $code_location
698
     *
699
     * @return bool
700
     */
701
    public function classOrInterfaceExists(
702
        $fq_class_name,
703
        CodeLocation $code_location = null,
704
        ?string $calling_fq_class_name = null,
705
        ?string $calling_method_id = null
706
    ) {
707
        return $this->classlikes->classOrInterfaceExists(
708
            $fq_class_name,
709
            $code_location,
710
            $calling_fq_class_name,
711
            $calling_method_id
712
        );
713
    }
714
715
    /**
716
     * @param  string       $fq_class_name
717
     * @param  string       $possible_parent
718
     *
719
     * @return bool
720
     */
721
    public function classExtendsOrImplements($fq_class_name, $possible_parent)
722
    {
723
        return $this->classlikes->classExtends($fq_class_name, $possible_parent)
724
            || $this->classlikes->classImplements($fq_class_name, $possible_parent);
725
    }
726
727
    /**
728
     * Determine whether or not a given class exists
729
     *
730
     * @param  string       $fq_class_name
731
     *
732
     * @return bool
733
     */
734
    public function classExists(
735
        $fq_class_name,
736
        CodeLocation $code_location = null,
737
        ?string $calling_fq_class_name = null,
738
        ?string $calling_method_id = null
739
    ) {
740
        return $this->classlikes->classExists(
741
            $fq_class_name,
742
            $code_location,
743
            $calling_fq_class_name,
744
            $calling_method_id
745
        );
746
    }
747
748
    /**
749
     * Determine whether or not a class extends a parent
750
     *
751
     * @param  string       $fq_class_name
752
     * @param  string       $possible_parent
753
     *
754
     * @throws \Psalm\Exception\UnpopulatedClasslikeException when called on unpopulated class
755
     * @throws \InvalidArgumentException when class does not exist
756
     *
757
     * @return bool
758
     */
759
    public function classExtends($fq_class_name, $possible_parent)
760
    {
761
        return $this->classlikes->classExtends($fq_class_name, $possible_parent, true);
762
    }
763
764
    /**
765
     * Check whether a class implements an interface
766
     *
767
     * @param  string       $fq_class_name
768
     * @param  string       $interface
769
     *
770
     * @return bool
771
     */
772
    public function classImplements($fq_class_name, $interface)
773
    {
774
        return $this->classlikes->classImplements($fq_class_name, $interface);
775
    }
776
777
    /**
778
     * @param  string         $fq_interface_name
779
     *
780
     * @return bool
781
     */
782
    public function interfaceExists(
783
        $fq_interface_name,
784
        CodeLocation $code_location = null,
785
        ?string $calling_fq_class_name = null,
786
        ?string $calling_method_id = null
787
    ) {
788
        return $this->classlikes->interfaceExists(
789
            $fq_interface_name,
790
            $code_location,
791
            $calling_fq_class_name,
792
            $calling_method_id
793
        );
794
    }
795
796
    /**
797
     * @param  string         $interface_name
798
     * @param  string         $possible_parent
799
     *
800
     * @return bool
801
     */
802
    public function interfaceExtends($interface_name, $possible_parent)
803
    {
804
        return $this->classlikes->interfaceExtends($interface_name, $possible_parent);
805
    }
806
807
    /**
808
     * @param  string         $fq_interface_name
809
     *
810
     * @return array<string>   all interfaces extended by $interface_name
811
     */
812
    public function getParentInterfaces($fq_interface_name)
813
    {
814
        return $this->classlikes->getParentInterfaces(
815
            $this->classlikes->getUnAliasedName($fq_interface_name)
816
        );
817
    }
818
819
    /**
820
     * Determine whether or not a class has the correct casing
821
     *
822
     * @param  string $fq_class_name
823
     *
824
     * @return bool
825
     */
826
    public function classHasCorrectCasing($fq_class_name)
827
    {
828
        return $this->classlikes->classHasCorrectCasing($fq_class_name);
829
    }
830
831
    /**
832
     * @param  string $fq_interface_name
833
     *
834
     * @return bool
835
     */
836
    public function interfaceHasCorrectCasing($fq_interface_name)
837
    {
838
        return $this->classlikes->interfaceHasCorrectCasing($fq_interface_name);
839
    }
840
841
    /**
842
     * @param  string $fq_trait_name
843
     *
844
     * @return bool
845
     */
846
    public function traitHasCorrectCase($fq_trait_name)
847
    {
848
        return $this->classlikes->traitHasCorrectCase($fq_trait_name);
849
    }
850
851
    /**
852
     * Given a function id, return the function like storage for
853
     * a method, closure, or function.
854
     *
855
     * @param non-empty-string $function_id
0 ignored issues
show
Documentation introduced by
The doc-type non-empty-string could not be parsed: Unknown type name "non-empty-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...
856
     */
857
    public function getFunctionLikeStorage(
858
        StatementsAnalyzer $statements_analyzer,
859
        string $function_id
860
    ): FunctionLikeStorage {
861
        $doesMethodExist =
862
            \Psalm\Internal\MethodIdentifier::isValidMethodIdReference($function_id)
863
            && $this->methodExists($function_id);
864
865
        if ($doesMethodExist) {
866
            $method_id = \Psalm\Internal\MethodIdentifier::wrap($function_id);
867
868
            $declaring_method_id = $this->methods->getDeclaringMethodId($method_id);
869
870
            if (!$declaring_method_id) {
871
                throw new \UnexpectedValueException('Declaring method for ' . $method_id . ' cannot be found');
872
            }
873
874
            return $this->methods->getStorage($declaring_method_id);
875
        }
876
877
        return $this->functions->getStorage($statements_analyzer, strtolower($function_id));
878
    }
879
880
    /**
881
     * Whether or not a given method exists
882
     *
883
     * @param  string|\Psalm\Internal\MethodIdentifier       $method_id
884
     * @param  string|\Psalm\Internal\MethodIdentifier|null $calling_method_id
885
     *
886
     @return bool
887
     */
888
    public function methodExists(
889
        $method_id,
890
        ?CodeLocation $code_location = null,
891
        $calling_method_id = null,
892
        ?string $file_path = null
893
    ) {
894
        return $this->methods->methodExists(
895
            Internal\MethodIdentifier::wrap($method_id),
896
            is_string($calling_method_id) ? strtolower($calling_method_id) : strtolower((string) $calling_method_id),
897
            $code_location,
898
            null,
899
            $file_path
900
        );
901
    }
902
903
    /**
904
     * @param  string|\Psalm\Internal\MethodIdentifier $method_id
905
     *
906
     * @return array<int, \Psalm\Storage\FunctionLikeParameter>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, 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...
907
     */
908
    public function getMethodParams($method_id)
909
    {
910
        return $this->methods->getMethodParams(Internal\MethodIdentifier::wrap($method_id));
911
    }
912
913
    /**
914
     * @param  string|\Psalm\Internal\MethodIdentifier $method_id
915
     *
916
     * @return bool
917
     */
918
    public function isVariadic($method_id)
919
    {
920
        return $this->methods->isVariadic(Internal\MethodIdentifier::wrap($method_id));
921
    }
922
923
    /**
924
     * @param  string|\Psalm\Internal\MethodIdentifier $method_id
925
     * @param  string $self_class
926
     * @param  array<int, PhpParser\Node\Arg> $call_args
927
     *
928
     * @return Type\Union|null
929
     */
930
    public function getMethodReturnType($method_id, &$self_class, array $call_args = [])
931
    {
932
        return $this->methods->getMethodReturnType(
933
            Internal\MethodIdentifier::wrap($method_id),
934
            $self_class,
935
            null,
936
            $call_args
937
        );
938
    }
939
940
    /**
941
     * @param  string|\Psalm\Internal\MethodIdentifier $method_id
942
     *
943
     * @return bool
944
     */
945
    public function getMethodReturnsByRef($method_id)
946
    {
947
        return $this->methods->getMethodReturnsByRef(Internal\MethodIdentifier::wrap($method_id));
948
    }
949
950
    /**
951
     * @param  string|\Psalm\Internal\MethodIdentifier   $method_id
952
     * @param  CodeLocation|null    $defined_location
953
     *
954
     * @return CodeLocation|null
955
     */
956
    public function getMethodReturnTypeLocation(
957
        $method_id,
958
        CodeLocation &$defined_location = null
959
    ) {
960
        return $this->methods->getMethodReturnTypeLocation(
961
            Internal\MethodIdentifier::wrap($method_id),
962
            $defined_location
963
        );
964
    }
965
966
    /**
967
     * @param  string|\Psalm\Internal\MethodIdentifier $method_id
968
     *
969
     * @return string|null
970
     */
971
    public function getDeclaringMethodId($method_id)
972
    {
973
        return $this->methods->getDeclaringMethodId(Internal\MethodIdentifier::wrap($method_id));
974
    }
975
976
    /**
977
     * Get the class this method appears in (vs is declared in, which could give a trait)
978
     *
979
     * @param  string|\Psalm\Internal\MethodIdentifier $method_id
980
     *
981
     * @return string|null
982
     */
983
    public function getAppearingMethodId($method_id)
984
    {
985
        return $this->methods->getAppearingMethodId(Internal\MethodIdentifier::wrap($method_id));
986
    }
987
988
    /**
989
     * @param  string|\Psalm\Internal\MethodIdentifier $method_id
990
     *
991
     * @return array<string>
992
     */
993
    public function getOverriddenMethodIds($method_id)
994
    {
995
        return $this->methods->getOverriddenMethodIds(Internal\MethodIdentifier::wrap($method_id));
996
    }
997
998
    /**
999
     * @param  string|\Psalm\Internal\MethodIdentifier $method_id
1000
     *
1001
     * @return string
1002
     */
1003
    public function getCasedMethodId($method_id)
1004
    {
1005
        return $this->methods->getCasedMethodId(Internal\MethodIdentifier::wrap($method_id));
1006
    }
1007
1008
    /**
1009
     * @param string $file_path
1010
     *
1011
     * @return void
1012
     */
1013
    public function invalidateInformationForFile(string $file_path)
1014
    {
1015
        $this->scanner->removeFile($file_path);
1016
1017
        try {
1018
            $file_storage = $this->file_storage_provider->get($file_path);
1019
        } catch (\InvalidArgumentException $e) {
1020
            return;
1021
        }
1022
1023
        foreach ($file_storage->classlikes_in_file as $fq_classlike_name) {
1024
            $this->classlike_storage_provider->remove($fq_classlike_name);
1025
            $this->classlikes->removeClassLike($fq_classlike_name);
1026
        }
1027
1028
        $this->file_storage_provider->remove($file_path);
1029
    }
1030
1031
    /**
1032
     * @return ?string
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?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...
1033
     */
1034
    public function getSymbolInformation(string $file_path, string $symbol)
1035
    {
1036
        if (\is_numeric($symbol[0])) {
1037
            return \preg_replace('/^[^:]*:/', '', $symbol);
1038
        }
1039
1040
        try {
1041
            if (strpos($symbol, '::')) {
1042
                if (strpos($symbol, '()')) {
1043
                    $symbol = substr($symbol, 0, -2);
1044
1045
                    $method_id = new \Psalm\Internal\MethodIdentifier(...explode('::', $symbol));
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
\explode('::', $symbol) 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...
1046
1047
                    $declaring_method_id = $this->methods->getDeclaringMethodId($method_id);
1048
1049
                    if (!$declaring_method_id) {
1050
                        return null;
1051
                    }
1052
1053
                    $storage = $this->methods->getStorage($declaring_method_id);
1054
1055
                    return '<?php ' . $storage->getSignature(true);
1056
                }
1057
1058
                list(, $symbol_name) = explode('::', $symbol);
1059
1060
                if (strpos($symbol, '$') !== false) {
1061
                    $storage = $this->properties->getStorage($symbol);
1062
1063
                    return '<?php ' . $storage->getInfo() . ' ' . $symbol_name;
1064
                }
1065
1066
                list($fq_classlike_name, $const_name) = explode('::', $symbol);
1067
1068
                $class_constants = $this->classlikes->getConstantsForClass(
1069
                    $fq_classlike_name,
1070
                    \ReflectionProperty::IS_PRIVATE
1071
                );
1072
1073
                if (!isset($class_constants[$const_name])) {
1074
                    return null;
1075
                }
1076
1077
                return '<?php ' . $const_name;
1078
            }
1079
1080
            if (strpos($symbol, '()')) {
1081
                $function_id = strtolower(substr($symbol, 0, -2));
1082
                $file_storage = $this->file_storage_provider->get($file_path);
1083
1084
                if (isset($file_storage->functions[$function_id])) {
1085
                    $function_storage = $file_storage->functions[$function_id];
1086
1087
                    return '<?php ' . $function_storage->getSignature(true);
1088
                }
1089
1090
                if (!$function_id) {
1091
                    return null;
1092
                }
1093
1094
                $function = $this->functions->getStorage(null, $function_id);
1095
                return '<?php ' . $function->getSignature(true);
1096
            }
1097
1098
            $storage = $this->classlike_storage_provider->get($symbol);
1099
1100
            return '<?php ' . ($storage->abstract ? 'abstract ' : '') . 'class ' . $storage->name;
1101
        } catch (\Exception $e) {
1102
            error_log($e->getMessage());
1103
1104
            return null;
1105
        }
1106
    }
1107
1108
    /**
1109
     * @return ?CodeLocation
0 ignored issues
show
Documentation introduced by
The doc-type ?CodeLocation could not be parsed: Unknown type name "?CodeLocation" 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...
1110
     */
1111
    public function getSymbolLocation(string $file_path, string $symbol)
1112
    {
1113
        if (\is_numeric($symbol[0])) {
1114
            $symbol = \preg_replace('/:.*/', '', $symbol);
1115
            $symbol_parts = explode('-', $symbol);
1116
1117
            $file_contents = $this->getFileContents($file_path);
1118
1119
            return new CodeLocation\Raw(
1120
                $file_contents,
1121
                $file_path,
1122
                $this->config->shortenFileName($file_path),
1123
                (int) $symbol_parts[0],
1124
                (int) $symbol_parts[1]
1125
            );
1126
        }
1127
1128
        try {
1129
            if (strpos($symbol, '::')) {
1130
                if (strpos($symbol, '()')) {
1131
                    $symbol = substr($symbol, 0, -2);
1132
1133
                    $method_id = new \Psalm\Internal\MethodIdentifier(...explode('::', $symbol));
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
\explode('::', $symbol) 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...
1134
1135
                    $declaring_method_id = $this->methods->getDeclaringMethodId($method_id);
1136
1137
                    if (!$declaring_method_id) {
1138
                        return null;
1139
                    }
1140
1141
                    $storage = $this->methods->getStorage($declaring_method_id);
1142
1143
                    return $storage->location;
1144
                }
1145
1146
                if (strpos($symbol, '$') !== false) {
1147
                    $storage = $this->properties->getStorage($symbol);
1148
1149
                    return $storage->location;
1150
                }
1151
1152
                list($fq_classlike_name, $const_name) = explode('::', $symbol);
1153
1154
                $class_constants = $this->classlikes->getConstantsForClass(
1155
                    $fq_classlike_name,
1156
                    \ReflectionProperty::IS_PRIVATE
1157
                );
1158
1159
                if (!isset($class_constants[$const_name])) {
1160
                    return null;
1161
                }
1162
1163
                $class_const_storage = $this->classlike_storage_provider->get($fq_classlike_name);
1164
1165
                return $class_const_storage->class_constant_locations[$const_name];
1166
            }
1167
1168
            if (strpos($symbol, '()')) {
1169
                $file_storage = $this->file_storage_provider->get($file_path);
1170
1171
                $function_id = strtolower(substr($symbol, 0, -2));
1172
1173
                if (isset($file_storage->functions[$function_id])) {
1174
                    return $file_storage->functions[$function_id]->location;
1175
                }
1176
1177
                if (!$function_id) {
1178
                    return null;
1179
                }
1180
1181
                $function = $this->functions->getStorage(null, $function_id);
1182
                return $function->location;
1183
            }
1184
1185
            $storage = $this->classlike_storage_provider->get($symbol);
1186
1187
            return $storage->location;
1188
        } catch (\UnexpectedValueException $e) {
1189
            error_log($e->getMessage());
1190
1191
            return null;
1192
        } catch (\InvalidArgumentException $e) {
1193
            return null;
1194
        }
1195
    }
1196
1197
    /**
1198
     * @return array{0: string, 1: Range}|null
0 ignored issues
show
Documentation introduced by
The doc-type array{0: could not be parsed: Unknown type name "array{0:" 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...
1199
     */
1200
    public function getReferenceAtPosition(string $file_path, Position $position)
1201
    {
1202
        $is_open = $this->file_provider->isOpen($file_path);
1203
1204
        if (!$is_open) {
1205
            throw new \Psalm\Exception\UnanalyzedFileException($file_path . ' is not open');
1206
        }
1207
1208
        $file_contents = $this->getFileContents($file_path);
1209
1210
        $offset = $position->toOffset($file_contents);
1211
1212
        list($reference_map, $type_map) = $this->analyzer->getMapsForFile($file_path);
1213
1214
        $reference = null;
1215
1216
        if (!$reference_map && !$type_map) {
1217
            return null;
1218
        }
1219
1220
        $reference_start_pos = null;
1221
        $reference_end_pos = null;
1222
1223
        ksort($reference_map);
1224
1225
        foreach ($reference_map as $start_pos => list($end_pos, $possible_reference)) {
1226
            if ($offset < $start_pos) {
1227
                break;
1228
            }
1229
1230
            if ($offset > $end_pos) {
1231
                continue;
1232
            }
1233
            $reference_start_pos = $start_pos;
1234
            $reference_end_pos = $end_pos;
1235
            $reference = $possible_reference;
1236
        }
1237
1238
        if ($reference === null || $reference_start_pos === null || $reference_end_pos === null) {
1239
            return null;
1240
        }
1241
1242
        $range = new Range(
1243
            self::getPositionFromOffset($reference_start_pos, $file_contents),
1244
            self::getPositionFromOffset($reference_end_pos, $file_contents)
1245
        );
1246
1247
        return [$reference, $range];
1248
    }
1249
1250
    /**
1251
     * @return array{0: non-empty-string, 1: int, 2: Range}|null
0 ignored issues
show
Documentation introduced by
The doc-type array{0: could not be parsed: Unknown type name "array{0:" 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...
1252
     */
1253
    public function getFunctionArgumentAtPosition(string $file_path, Position $position)
1254
    {
1255
        $is_open = $this->file_provider->isOpen($file_path);
1256
1257
        if (!$is_open) {
1258
            throw new \Psalm\Exception\UnanalyzedFileException($file_path . ' is not open');
1259
        }
1260
1261
        $file_contents = $this->getFileContents($file_path);
1262
1263
        $offset = $position->toOffset($file_contents);
1264
1265
        list(, , $argument_map) = $this->analyzer->getMapsForFile($file_path);
1266
1267
        $reference = null;
1268
        $argument_number = null;
1269
1270
        if (!$argument_map) {
1271
            return null;
1272
        }
1273
1274
        $start_pos = null;
1275
        $end_pos = null;
1276
1277
        ksort($argument_map);
1278
1279
        foreach ($argument_map as $start_pos => list($end_pos, $possible_reference, $possible_argument_number)) {
1280
            if ($offset < $start_pos) {
1281
                break;
1282
            }
1283
1284
            if ($offset > $end_pos) {
1285
                continue;
1286
            }
1287
1288
            $reference = $possible_reference;
1289
            $argument_number = $possible_argument_number;
1290
        }
1291
1292
        if ($reference === null || $start_pos === null || $end_pos === null || $argument_number === null) {
1293
            return null;
1294
        }
1295
1296
        $range = new Range(
1297
            self::getPositionFromOffset($start_pos, $file_contents),
1298
            self::getPositionFromOffset($end_pos, $file_contents)
1299
        );
1300
1301
        return [$reference, $argument_number, $range];
1302
    }
1303
1304
    /**
1305
     * @param  non-empty-string $function_symbol
0 ignored issues
show
Documentation introduced by
The doc-type non-empty-string could not be parsed: Unknown type name "non-empty-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...
1306
     */
1307
    public function getSignatureInformation(string $function_symbol) : ?\LanguageServerProtocol\SignatureInformation
1308
    {
1309
        if (strpos($function_symbol, '::') !== false) {
1310
            $method_id = new \Psalm\Internal\MethodIdentifier(...explode('::', $function_symbol));
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
\explode('::', $function_symbol) 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...
1311
1312
            $declaring_method_id = $this->methods->getDeclaringMethodId($method_id);
1313
1314
            if ($declaring_method_id === null) {
1315
                return null;
1316
            }
1317
1318
            $method_storage = $this->methods->getStorage($declaring_method_id);
1319
            $params = $method_storage->params;
1320
        } else {
1321
            try {
1322
                $function_storage = $this->functions->getStorage(null, strtolower($function_symbol));
1323
1324
                $params = $function_storage->params;
1325
            } catch (\Exception $exception) {
1326
                if (InternalCallMapHandler::inCallMap($function_symbol)) {
1327
                    $callables = InternalCallMapHandler::getCallablesFromCallMap($function_symbol);
1328
1329
                    if (!$callables || !$callables[0]->params) {
1330
                        return null;
1331
                    }
1332
1333
                    $params = $callables[0]->params;
1334
                } else {
1335
                    return null;
1336
                }
1337
            }
1338
        }
1339
1340
        $signature_label = '(';
1341
        $parameters = [];
1342
1343
        foreach ($params as $i => $param) {
1344
            $parameter_label = ($param->type ?: 'mixed') . ' $' . $param->name;
1345
            $parameters[] = new \LanguageServerProtocol\ParameterInformation([
1346
                strlen($signature_label),
1347
                strlen($signature_label) + strlen($parameter_label),
1348
            ]);
1349
1350
            $signature_label .= $parameter_label;
1351
1352
            if ($i < (count($params) - 1)) {
1353
                $signature_label .= ', ';
1354
            }
1355
        }
1356
1357
        $signature_label .= ')';
1358
1359
        return new \LanguageServerProtocol\SignatureInformation(
1360
            $signature_label,
1361
            $parameters
1362
        );
1363
    }
1364
1365
    /**
1366
     * @return array{0: string, 1: '->'|'::'|'symbol', 2: int}|null
0 ignored issues
show
Documentation introduced by
The doc-type array{0: could not be parsed: Unknown type name "array{0:" 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...
1367
     */
1368
    public function getCompletionDataAtPosition(string $file_path, Position $position)
1369
    {
1370
        $is_open = $this->file_provider->isOpen($file_path);
1371
1372
        if (!$is_open) {
1373
            throw new \Psalm\Exception\UnanalyzedFileException($file_path . ' is not open');
1374
        }
1375
1376
        $file_contents = $this->getFileContents($file_path);
1377
1378
        $offset = $position->toOffset($file_contents);
1379
1380
        list($reference_map, $type_map) = $this->analyzer->getMapsForFile($file_path);
1381
1382
        if (!$reference_map && !$type_map) {
1383
            return null;
1384
        }
1385
1386
        krsort($type_map);
1387
1388
        foreach ($type_map as $start_pos => list($end_pos_excluding_whitespace, $possible_type)) {
1389
            if ($offset < $start_pos) {
1390
                continue;
1391
            }
1392
1393
            $num_whitespace_bytes = preg_match('/\G\s+/', $file_contents, $matches, 0, $end_pos_excluding_whitespace)
1394
                ? strlen($matches[0])
1395
                : 0;
1396
            $end_pos = $end_pos_excluding_whitespace + $num_whitespace_bytes;
1397
1398
            if ($offset - $end_pos === 2 || $offset - $end_pos === 3) {
1399
                $candidate_gap = substr($file_contents, $end_pos, 2);
1400
1401
                if ($candidate_gap === '->' || $candidate_gap === '::') {
1402
                    $gap = $candidate_gap;
1403
                    $recent_type = $possible_type;
1404
1405
                    if ($recent_type === 'mixed') {
1406
                        return null;
1407
                    }
1408
1409
                    return [$recent_type, $gap, $offset];
1410
                }
1411
            }
1412
        }
1413
1414
        foreach ($reference_map as $start_pos => list($end_pos, $possible_reference)) {
1415
            if ($offset < $start_pos || $possible_reference[0] !== '*') {
1416
                continue;
1417
            }
1418
1419
            if ($offset - $end_pos === 0) {
1420
                $recent_type = $possible_reference;
1421
1422
                return [$recent_type, 'symbol', $offset];
1423
            }
1424
        }
1425
1426
        return null;
1427
    }
1428
1429
    /**
1430
     * @return array<int, \LanguageServerProtocol\CompletionItem>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, 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...
1431
     */
1432
    public function getCompletionItemsForClassishThing(string $type_string, string $gap) : array
1433
    {
1434
        $instance_completion_items = [];
1435
        $static_completion_items = [];
1436
1437
        $type = Type::parseString($type_string);
1438
1439
        foreach ($type->getAtomicTypes() as $atomic_type) {
1440
            if ($atomic_type instanceof Type\Atomic\TNamedObject) {
1441
                try {
1442
                    $class_storage = $this->classlike_storage_provider->get($atomic_type->value);
1443
1444
                    foreach ($class_storage->appearing_method_ids as $declaring_method_id) {
1445
                        $method_storage = $this->methods->getStorage($declaring_method_id);
1446
1447
                        $completion_item = new \LanguageServerProtocol\CompletionItem(
1448
                            $method_storage->cased_name,
1449
                            \LanguageServerProtocol\CompletionItemKind::METHOD,
1450
                            (string)$method_storage,
1451
                            null,
1452
                            (string)$method_storage->visibility,
1453
                            $method_storage->cased_name,
1454
                            $method_storage->cased_name . (count($method_storage->params) !== 0 ? '($0)' : '()'),
1455
                            null,
1456
                            null,
1457
                            new Command('Trigger parameter hints', 'editor.action.triggerParameterHints')
1458
                        );
1459
                        $completion_item->insertTextFormat = \LanguageServerProtocol\InsertTextFormat::SNIPPET;
1460
1461
                        if ($method_storage->is_static) {
1462
                            $static_completion_items[] = $completion_item;
1463
                        } else {
1464
                            $instance_completion_items[] = $completion_item;
1465
                        }
1466
                    }
1467
1468
                    foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) {
1469
                        $property_storage = $this->properties->getStorage(
1470
                            $declaring_class . '::$' . $property_name
1471
                        );
1472
1473
                        $completion_item = new \LanguageServerProtocol\CompletionItem(
1474
                            '$' . $property_name,
1475
                            \LanguageServerProtocol\CompletionItemKind::PROPERTY,
1476
                            $property_storage->getInfo(),
1477
                            null,
1478
                            (string)$property_storage->visibility,
1479
                            $property_name,
1480
                            ($gap === '::' ? '$' : '') . $property_name
1481
                        );
1482
1483
                        if ($property_storage->is_static) {
1484
                            $static_completion_items[] = $completion_item;
1485
                        } else {
1486
                            $instance_completion_items[] = $completion_item;
1487
                        }
1488
                    }
1489
1490
                    foreach ($class_storage->class_constant_locations as $const_name => $_) {
1491
                        $static_completion_items[] = new \LanguageServerProtocol\CompletionItem(
1492
                            $const_name,
1493
                            \LanguageServerProtocol\CompletionItemKind::VARIABLE,
1494
                            'const ' . $const_name,
1495
                            null,
1496
                            null,
1497
                            $const_name,
1498
                            $const_name
1499
                        );
1500
                    }
1501
                } catch (\Exception $e) {
1502
                    error_log($e->getMessage());
1503
                    continue;
1504
                }
1505
            }
1506
        }
1507
1508
        if ($gap === '->') {
1509
            $completion_items = $instance_completion_items;
1510
        } else {
1511
            $completion_items = array_merge(
1512
                $instance_completion_items,
1513
                $static_completion_items
1514
            );
1515
        }
1516
1517
        return $completion_items;
1518
    }
1519
1520
    /**
1521
     * @return array<int, \LanguageServerProtocol\CompletionItem>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, 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...
1522
     */
1523
    public function getCompletionItemsForPartialSymbol(
1524
        string $type_string,
1525
        int $offset,
1526
        string $file_path
1527
    ) : array {
1528
        $matching_classlike_names = $this->classlikes->getMatchingClassLikeNames($type_string);
1529
1530
        $completion_items = [];
1531
1532
        $file_storage = $this->file_storage_provider->get($file_path);
1533
1534
        $aliases = null;
1535
1536
        foreach ($file_storage->classlikes_in_file as $fq_class_name => $_) {
1537
            try {
1538
                $class_storage = $this->classlike_storage_provider->get($fq_class_name);
1539
            } catch (\Exception $e) {
1540
                continue;
1541
            }
1542
1543
            if (!$class_storage->stmt_location) {
1544
                continue;
1545
            }
1546
1547
            if ($offset > $class_storage->stmt_location->raw_file_start
1548
                && $offset < $class_storage->stmt_location->raw_file_end
1549
            ) {
1550
                $aliases = $class_storage->aliases;
1551
                break;
1552
            }
1553
        }
1554
1555
        if (!$aliases) {
1556
            foreach ($file_storage->namespace_aliases as $namespace_start => $namespace_aliases) {
1557
                if ($namespace_start < $offset) {
1558
                    $aliases = $namespace_aliases;
1559
                    break;
1560
                }
1561
            }
1562
1563
            if (!$aliases) {
1564
                $aliases = $file_storage->aliases;
1565
            }
1566
        }
1567
1568
        foreach ($matching_classlike_names as $fq_class_name) {
1569
            $extra_edits = [];
1570
1571
            $insertion_text = Type::getStringFromFQCLN(
1572
                $fq_class_name,
1573
                $aliases && $aliases->namespace ? $aliases->namespace : null,
1574
                $aliases ? $aliases->uses_flipped : [],
1575
                null
1576
            );
1577
1578
            if ($aliases
1579
                && $aliases->namespace
1580
                && $insertion_text === '\\' . $fq_class_name
1581
                && $aliases->namespace_first_stmt_start
1582
            ) {
1583
                $file_contents = $this->getFileContents($file_path);
1584
1585
                $class_name = \preg_replace('/^.*\\\/', '', $fq_class_name);
1586
1587
                if ($aliases->uses_end) {
1588
                    $position = self::getPositionFromOffset($aliases->uses_end, $file_contents);
1589
                    $extra_edits[] = new \LanguageServerProtocol\TextEdit(
1590
                        new Range(
1591
                            $position,
1592
                            $position
1593
                        ),
1594
                        "\n" . 'use ' . $fq_class_name . ';'
1595
                    );
1596
                } else {
1597
                    $position = self::getPositionFromOffset($aliases->namespace_first_stmt_start, $file_contents);
1598
                    $extra_edits[] = new \LanguageServerProtocol\TextEdit(
1599
                        new Range(
1600
                            $position,
1601
                            $position
1602
                        ),
1603
                        'use ' . $fq_class_name . ';' . "\n" . "\n"
1604
                    );
1605
                }
1606
1607
                $insertion_text = $class_name;
1608
            }
1609
1610
            $completion_items[] = new \LanguageServerProtocol\CompletionItem(
1611
                $fq_class_name,
1612
                \LanguageServerProtocol\CompletionItemKind::CLASS_,
1613
                null,
1614
                null,
1615
                null,
1616
                $fq_class_name,
1617
                $insertion_text,
1618
                null,
1619
                $extra_edits
1620
            );
1621
        }
1622
1623
        return $completion_items;
1624
    }
1625
1626
    private static function getPositionFromOffset(int $offset, string $file_contents) : Position
1627
    {
1628
        $file_contents = substr($file_contents, 0, $offset);
1629
1630
        $before_newline_count = strrpos($file_contents, "\n", $offset - strlen($file_contents));
1631
1632
        return new Position(
1633
            substr_count($file_contents, "\n"),
1634
            $offset - (int)$before_newline_count - 1
1635
        );
1636
    }
1637
1638
    /**
1639
     * @return void
1640
     */
1641
    public function addTemporaryFileChanges(string $file_path, string $new_content)
1642
    {
1643
        $this->file_provider->addTemporaryFileChanges($file_path, $new_content);
1644
    }
1645
1646
    /**
1647
     * @return void
1648
     */
1649
    public function removeTemporaryFileChanges(string $file_path)
1650
    {
1651
        $this->file_provider->removeTemporaryFileChanges($file_path);
1652
    }
1653
1654
    /**
1655
     * Checks if type is a subtype of other
1656
     *
1657
     * Given two types, checks if `$input_type` is a subtype of `$container_type`.
1658
     * If you consider `Type\Union` as a set of types, this will tell you
1659
     * if `$input_type` is fully contained in `$container_type`,
1660
     *
1661
     * $input_type ⊆ $container_type
1662
     *
1663
     * Useful for emitting issues like InvalidArgument, where argument at the call site
1664
     * should be a subset of the function parameter type.
1665
     */
1666
    public function isTypeContainedByType(
1667
        Type\Union $input_type,
1668
        Type\Union $container_type
1669
    ): bool {
1670
        return TypeAnalyzer::isContainedBy($this, $input_type, $container_type);
1671
    }
1672
1673
    /**
1674
     * Checks if type has any part that is a subtype of other
1675
     *
1676
     * Given two types, checks if *any part* of `$input_type` is a subtype of `$container_type`.
1677
     * If you consider `Type\Union` as a set of types, this will tell you if intersection
1678
     * of `$input_type` with `$container_type` is not empty.
1679
     *
1680
     * $input_type ∩ $container_type ≠ ∅ , e.g. they are not disjoint.
1681
     *
1682
     * Useful for emitting issues like PossiblyInvalidArgument, where argument at the call
1683
     * site should be a subtype of the function parameter type, but it's has some types that are
1684
     * not a subtype of the required type.
1685
     */
1686
    public function canTypeBeContainedByType(
1687
        Type\Union $input_type,
1688
        Type\Union $container_type
1689
    ): bool {
1690
        return TypeAnalyzer::canBeContainedBy($this, $input_type, $container_type);
1691
    }
1692
1693
    /**
1694
     * Extracts key and value types from a traversable object (or iterable)
1695
     *
1696
     * Given an iterable type (*but not TArray*) returns a tuple of it's key/value types.
1697
     * First element of the tuple holds key type, second has the value type.
1698
     *
1699
     * Example:
1700
     * ```php
1701
     * $codebase->getKeyValueParamsForTraversableObject(Type::parseString('iterable<int,string>'))
1702
     * //  returns [Union(TInt), Union(TString)]
1703
     * ```
1704
     *
1705
     * @return array{Type\Union,Type\Union}
0 ignored issues
show
Documentation introduced by
The doc-type array{Type\Union,Type\Union} could not be parsed: Unknown type name "array{Type\Union" 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...
1706
     */
1707
    public function getKeyValueParamsForTraversableObject(Type\Atomic $type): array
1708
    {
1709
        $key_type = null;
1710
        $value_type = null;
1711
1712
        ForeachAnalyzer::getKeyValueParamsForTraversableObject($type, $this, $key_type, $value_type);
1713
1714
        return [
1715
            $key_type ?? Type::getMixed(),
1716
            $value_type ?? Type::getMixed(),
1717
        ];
1718
    }
1719
1720
    /**
1721
     * @param array<string, mixed> $phantom_classes
1722
     * @psalm-suppress PossiblyUnusedMethod part of the public API
1723
     */
1724
    public function queueClassLikeForScanning(
1725
        string $fq_classlike_name,
1726
        bool $analyze_too = false,
1727
        bool $store_failure = true,
1728
        array $phantom_classes = []
1729
    ): void {
1730
        $this->scanner->queueClassLikeForScanning($fq_classlike_name, $analyze_too, $store_failure, $phantom_classes);
1731
    }
1732
1733
    /**
1734
     * @param array<string> $taints
1735
     *
1736
     * @psalm-suppress PossiblyUnusedMethod
1737
     */
1738
    public function addTaintSource(
1739
        Type\Union $expr_type,
1740
        string $taint_id,
1741
        array $taints = \Psalm\Type\TaintKindGroup::ALL_INPUT,
1742
        ?CodeLocation $code_location = null
1743
    ) : void {
1744
        if (!$this->taint) {
1745
            return;
1746
        }
1747
1748
        $source = new \Psalm\Internal\Taint\Source(
1749
            $taint_id,
1750
            $taint_id,
1751
            $code_location,
1752
            null,
1753
            $taints
1754
        );
1755
1756
        $this->taint->addSource($source);
1757
1758
        $expr_type->parent_nodes = [
1759
            $source,
1760
        ];
1761
    }
1762
1763
    /**
1764
     * @param array<string> $taints
1765
     *
1766
     * @psalm-suppress PossiblyUnusedMethod
1767
     */
1768
    public function addTaintSink(
1769
        string $taint_id,
1770
        array $taints = \Psalm\Type\TaintKindGroup::ALL_INPUT,
1771
        ?CodeLocation $code_location = null
1772
    ) : void {
1773
        if (!$this->taint) {
1774
            return;
1775
        }
1776
1777
        $sink = new \Psalm\Internal\Taint\Sink(
1778
            $taint_id,
1779
            $taint_id,
1780
            $code_location,
1781
            null,
1782
            $taints
1783
        );
1784
1785
        $this->taint->addSink($sink);
1786
    }
1787
}
1788