Issues (1236)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Psalm/Codebase.php (21 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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
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
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
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
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
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
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
The call to MethodIdentifier::__construct() misses a required argument $method_name.

This check looks for function calls that miss required arguments.

Loading history...
\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
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
The call to MethodIdentifier::__construct() misses a required argument $method_name.

This check looks for function calls that miss required arguments.

Loading history...
\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
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
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
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
The call to MethodIdentifier::__construct() misses a required argument $method_name.

This check looks for function calls that miss required arguments.

Loading history...
\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
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
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
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
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