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/Config.php (11 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 Composer\Semver\Semver;
5
use Psalm\Issue\VariableIssue;
6
use Webmozart\PathUtil\Path;
7
use function array_merge;
8
use function array_pop;
9
use function class_exists;
10
use Composer\Autoload\ClassLoader;
11
use DOMDocument;
12
use LogicException;
13
14
use function count;
15
use const DIRECTORY_SEPARATOR;
16
use function dirname;
17
use const E_USER_ERROR;
18
use function explode;
19
use function file_exists;
20
use function file_get_contents;
21
use function filetype;
22
use function get_class;
23
use function get_defined_constants;
24
use function get_defined_functions;
25
use function glob;
26
use function in_array;
27
use function intval;
28
use function is_dir;
29
use function is_file;
30
use function json_decode;
31
use function libxml_clear_errors;
32
use const GLOB_NOSORT;
33
use const LIBXML_ERR_ERROR;
34
use const LIBXML_ERR_FATAL;
35
use function libxml_get_errors;
36
use function libxml_use_internal_errors;
37
use function mkdir;
38
use const PHP_EOL;
39
use function phpversion;
40
use function preg_match;
41
use function preg_quote;
42
use function preg_replace;
43
use Psalm\Config\IssueHandler;
44
use Psalm\Config\ProjectFileFilter;
45
use Psalm\Config\TaintAnalysisFileFilter;
46
use Psalm\Exception\ConfigException;
47
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
48
use Psalm\Internal\Analyzer\FileAnalyzer;
49
use Psalm\Internal\Analyzer\ProjectAnalyzer;
50
use Psalm\Internal\IncludeCollector;
51
use Psalm\Internal\Scanner\FileScanner;
52
use Psalm\Issue\ArgumentIssue;
53
use Psalm\Issue\ClassIssue;
54
use Psalm\Issue\CodeIssue;
55
use Psalm\Issue\FunctionIssue;
56
use Psalm\Issue\MethodIssue;
57
use Psalm\Issue\PropertyIssue;
58
use Psalm\Plugin\Hook;
59
use Psalm\Progress\Progress;
60
use Psalm\Progress\VoidProgress;
61
use function realpath;
62
use function reset;
63
use function rmdir;
64
use function scandir;
65
use function sha1;
66
use SimpleXMLElement;
67
use function strpos;
68
use function strrpos;
69
use function strtolower;
70
use function strtr;
71
use function substr;
72
use function substr_count;
73
use function sys_get_temp_dir;
74
use function trigger_error;
75
use function unlink;
76
use function version_compare;
77
use function getcwd;
78
use function chdir;
79
use function simplexml_import_dom;
80
use const LIBXML_NONET;
81
use function is_a;
82
use const SCANDIR_SORT_NONE;
83
84
/**
85
 * @psalm-suppress PropertyNotSetInConstructor
86
 */
87
class Config
88
{
89
    const DEFAULT_FILE_NAME = 'psalm.xml';
90
    const REPORT_INFO = 'info';
91
    const REPORT_ERROR = 'error';
92
    const REPORT_SUPPRESS = 'suppress';
93
94
    /**
95
     * @var array<string>
96
     */
97
    public static $ERROR_LEVELS = [
98
        self::REPORT_INFO,
99
        self::REPORT_ERROR,
100
        self::REPORT_SUPPRESS,
101
    ];
102
103
    /**
104
     * @var array
105
     */
106
    const MIXED_ISSUES = [
107
        'MixedArgument',
108
        'MixedArrayAccess',
109
        'MixedArrayAssignment',
110
        'MixedArrayOffset',
111
        'MixedArrayTypeCoercion',
112
        'MixedAssignment',
113
        'MixedFunctionCall',
114
        'MixedInferredReturnType',
115
        'MixedMethodCall',
116
        'MixedOperand',
117
        'MixedPropertyFetch',
118
        'MixedPropertyAssignment',
119
        'MixedReturnStatement',
120
        'MixedStringOffsetAssignment',
121
        'MixedTypeCoercion',
122
        'MixedArgumentTypeCoercion',
123
        'MixedPropertyTypeCoercion',
124
        'MixedReturnTypeCoercion',
125
    ];
126
127
    /**
128
     * @var static|null
129
     */
130
    private static $instance;
131
132
    /**
133
     * Whether or not to use types as defined in docblocks
134
     *
135
     * @var bool
136
     */
137
    public $use_docblock_types = true;
138
139
    /**
140
     * Whether or not to use types as defined in property docblocks.
141
     * This is distinct from the above because you may want to use
142
     * property docblocks, but not function docblocks.
143
     *
144
     * @var bool
145
     */
146
    public $use_docblock_property_types = true;
147
148
    /**
149
     * Whether or not to throw an exception on first error
150
     *
151
     * @var bool
152
     */
153
    public $throw_exception = false;
154
155
    /**
156
     * Whether or not to load Xdebug stub
157
     *
158
     * @var bool|null
159
     */
160
    public $load_xdebug_stub = null;
161
162
    /**
163
     * The directory to store PHP Parser (and other) caches
164
     *
165
     * @var string
166
     */
167
    public $cache_directory;
168
169
    /**
170
     * The directory to store all Psalm project caches
171
     *
172
     * @var string|null
173
     */
174
    public $global_cache_directory;
175
176
    /**
177
     * Path to the autoader
178
     *
179
     * @var string|null
180
     */
181
    public $autoloader;
182
183
    /**
184
     * @var ProjectFileFilter|null
185
     */
186
    protected $project_files;
187
188
    /**
189
     * @var ProjectFileFilter|null
190
     */
191
    protected $extra_files;
192
193
    /**
194
     * The base directory of this config file
195
     *
196
     * @var string
197
     */
198
    public $base_dir;
199
200
    /**
201
     * The PHP version to assume as declared in the config file
202
     *
203
     * @var string|null
204
     */
205
    private $configured_php_version;
206
207
    /**
208
     * @var array<int, string>
209
     */
210
    private $file_extensions = ['php'];
211
212
    /**
213
     * @var array<string, class-string<FileScanner>>
214
     */
215
    private $filetype_scanners = [];
216
217
    /**
218
     * @var array<string, class-string<FileAnalyzer>>
219
     */
220
    private $filetype_analyzers = [];
221
222
    /**
223
     * @var array<string, string>
224
     */
225
    private $filetype_scanner_paths = [];
226
227
    /**
228
     * @var array<string, string>
229
     */
230
    private $filetype_analyzer_paths = [];
231
232
    /**
233
     * @var array<string, IssueHandler>
234
     */
235
    private $issue_handlers = [];
236
237
    /**
238
     * @var array<int, string>
239
     */
240
    private $mock_classes = [];
241
242
    /**
243
     * @var array<string, string>
244
     */
245
    private $stub_files = [];
246
247
    /**
248
     * @var bool
249
     */
250
    public $hide_external_errors = false;
251
252
    /** @var bool */
253
    public $allow_includes = true;
254
255
    /** @var 1|2|3|4|5|6|7|8 */
256
    public $level = 1;
257
258
    /**
259
     * @var ?bool
260
     */
261
    public $show_mixed_issues = null;
262
263
    /** @var bool */
264
    public $strict_binary_operands = false;
265
266
    /** @var bool */
267
    public $add_void_docblocks = true;
268
269
    /**
270
     * If true, assert() calls can be used to check types of variables
271
     *
272
     * @var bool
273
     */
274
    public $use_assert_for_type = true;
275
276
    /**
277
     * @var bool
278
     */
279
    public $remember_property_assignments_after_call = true;
280
281
    /** @var bool */
282
    public $use_igbinary = false;
283
284
    /**
285
     * @var bool
286
     */
287
    public $allow_phpstorm_generics = false;
288
289
    /**
290
     * @var bool
291
     */
292
    public $allow_string_standin_for_class = false;
293
294
    /**
295
     * @var bool
296
     */
297
    public $use_phpdoc_method_without_magic_or_parent = false;
298
299
    /**
300
     * @var bool
301
     */
302
    public $use_phpdoc_property_without_magic_or_parent = false;
303
304
    /**
305
     * @var bool
306
     */
307
    public $skip_checks_on_unresolvable_includes = true;
308
309
    /**
310
     * @var bool
311
     */
312
    public $seal_all_methods = false;
313
314
    /**
315
     * @var bool
316
     */
317
    public $memoize_method_calls = false;
318
319
    /**
320
     * @var bool
321
     */
322
    public $hoist_constants = false;
323
324
    /**
325
     * @var bool
326
     */
327
    public $add_param_default_to_docblock_type = false;
328
329
    /**
330
     * @var bool
331
     */
332
    public $check_for_throws_docblock = false;
333
334
    /**
335
     * @var bool
336
     */
337
    public $check_for_throws_in_global_scope = false;
338
339
    /**
340
     * @var bool
341
     */
342
    public $ignore_internal_falsable_issues = true;
343
344
    /**
345
     * @var bool
346
     */
347
    public $ignore_internal_nullable_issues = true;
348
349
    /**
350
     * @var array<string, bool>
351
     */
352
    public $ignored_exceptions = [];
353
354
    /**
355
     * @var array<string, bool>
356
     */
357
    public $ignored_exceptions_in_global_scope = [];
358
359
    /**
360
     * @var array<string, bool>
361
     */
362
    public $ignored_exceptions_and_descendants = [];
363
364
    /**
365
     * @var array<string, bool>
366
     */
367
    public $ignored_exceptions_and_descendants_in_global_scope = [];
368
369
    /**
370
     * @var bool
371
     */
372
    public $infer_property_types_from_constructor = true;
373
374
    /**
375
     * @var bool
376
     */
377
    public $ensure_array_string_offsets_exist = false;
378
379
    /**
380
     * @var bool
381
     */
382
    public $ensure_array_int_offsets_exist = false;
383
384
    /**
385
     * @var array<string, bool>
386
     */
387
    public $forbidden_functions = [];
388
389
    /**
390
     * @var bool
391
     */
392
    public $forbid_echo = false;
393
394
    /**
395
     * @var bool
396
     */
397
    public $find_unused_code = false;
398
399
    /**
400
     * @var bool
401
     */
402
    public $find_unused_variables = false;
403
404
    /**
405
     * @var bool
406
     */
407
    public $run_taint_analysis = false;
408
409
    /** @var bool */
410
    public $use_phpstorm_meta_path = true;
411
412
    /**
413
     * Whether to resolve file and directory paths from the location of the config file,
414
     * instead of the current working directory.
415
     *
416
     * @var bool
417
     */
418
    public $resolve_from_config_file = false;
419
420
    /**
421
     * @var string[]
422
     */
423
    public $plugin_paths = [];
424
425
    /**
426
     * @var array<array{class:string,config:?SimpleXMLElement}>
427
     */
428
    private $plugin_classes = [];
429
430
    /**
431
     * Static methods to be called after method checks have completed
432
     *
433
     * @var class-string<Hook\AfterMethodCallAnalysisInterface>[]
434
     */
435
    public $after_method_checks = [];
436
437
    /**
438
     * Static methods to be called after project function checks have completed
439
     *
440
     * Called after function calls to functions defined in the project.
441
     *
442
     * Allows influencing the return type and adding of modifications.
443
     *
444
     * @var class-string<Hook\AfterFunctionCallAnalysisInterface>[]
445
     */
446
    public $after_function_checks = [];
447
448
    /**
449
     * Static methods to be called after every function call
450
     *
451
     * Called after each function call, including php internal functions.
452
     *
453
     * Cannot change the call or influence its return type
454
     *
455
     * @var class-string<Hook\AfterEveryFunctionCallAnalysisInterface>[]
456
     */
457
    public $after_every_function_checks = [];
458
459
460
    /**
461
     * Static methods to be called after expression checks have completed
462
     *
463
     * @var class-string<Hook\AfterExpressionAnalysisInterface>[]
464
     */
465
    public $after_expression_checks = [];
466
467
    /**
468
     * Static methods to be called after statement checks have completed
469
     *
470
     * @var class-string<Hook\AfterStatementAnalysisInterface>[]
471
     */
472
    public $after_statement_checks = [];
473
474
    /**
475
     * Static methods to be called after method checks have completed
476
     *
477
     * @var class-string<Hook\StringInterpreterInterface>[]
478
     */
479
    public $string_interpreters = [];
480
481
    /**
482
     * Static methods to be called after classlike exists checks have completed
483
     *
484
     * @var class-string<Hook\AfterClassLikeExistenceCheckInterface>[]
485
     */
486
    public $after_classlike_exists_checks = [];
487
488
    /**
489
     * Static methods to be called after classlike checks have completed
490
     *
491
     * @var class-string<Hook\AfterClassLikeAnalysisInterface>[]
492
     */
493
    public $after_classlike_checks = [];
494
495
    /**
496
     * Static methods to be called after classlikes have been scanned
497
     *
498
     * @var class-string<Hook\AfterClassLikeVisitInterface>[]
499
     */
500
    public $after_visit_classlikes = [];
501
502
    /**
503
     * Static methods to be called after codebase has been populated
504
     *
505
     * @var class-string<Hook\AfterCodebasePopulatedInterface>[]
506
     */
507
    public $after_codebase_populated = [];
508
509
    /**
510
     * Static methods to be called after codebase has been populated
511
     *
512
     * @var class-string<Hook\AfterAnalysisInterface>[]
513
     */
514
    public $after_analysis = [];
515
516
    /**
517
     * Static methods to be called after codebase has been populated
518
     * @var class-string<Hook\BeforeAnalyzeFileInterface>[]
519
     */
520
    public $before_analyze_file = [];
521
522
    /**
523
     * Static methods to be called after functionlike checks have completed
524
     *
525
     * @var class-string<Hook\AfterFunctionLikeAnalysisInterface>[]
526
     */
527
    public $after_functionlike_checks = [];
528
529
    /** @var array<string, mixed> */
530
    private $predefined_constants;
531
532
    /** @var array<callable-string, bool> */
533
    private $predefined_functions = [];
534
535
    /** @var ClassLoader|null */
536
    private $composer_class_loader;
537
538
    /**
539
     * Custom functions that always exit
540
     *
541
     * @var array<string, bool>
542
     */
543
    public $exit_functions = [];
544
545
    /**
546
     * @var string
547
     */
548
    public $hash = '';
549
550
    /** @var string|null */
551
    public $error_baseline = null;
552
553
    /**
554
     * @var bool
555
     */
556
    public $include_php_versions_in_error_baseline = false;
557
558
    /** @var string */
559
    public $shepherd_host = 'shepherd.dev';
560
561
    /**
562
     * @var array<string, string>
563
     */
564
    public $globals = [];
565
566
    /**
567
     * @var bool
568
     */
569
    public $parse_sql = false;
570
571
    /**
572
     * @var int
573
     */
574
    public $max_string_length = 1000;
575
576
    /** @var ?IncludeCollector */
577
    private $include_collector;
578
579
    /**
580
     * @var TaintAnalysisFileFilter|null
581
     */
582
    protected $taint_analysis_ignored_files;
583
584
    /**
585
     * @var bool whether to emit a backtrace of emitted issues to stderr
586
     */
587
    public $debug_emitted_issues = false;
588
589
    protected function __construct()
590
    {
591
        self::$instance = $this;
592
    }
593
594
    /**
595
     * Gets a Config object from an XML file.
596
     *
597
     * Searches up a folder hierarchy for the most immediate config.
598
     *
599
     * @param  string $path
600
     * @param  string $current_dir
601
     * @param  string $output_format
602
     *
603
     * @return Config
604
     * @throws ConfigException if a config path is not found
605
     *
606
     */
607
    public static function getConfigForPath($path, $current_dir, $output_format)
608
    {
609
        $config_path = self::locateConfigFile($path);
610
611
        if (!$config_path) {
612
            if ($output_format === \Psalm\Report::TYPE_CONSOLE) {
613
                echo 'Could not locate a config XML file in path ' . $path
614
                    . '. Have you run \'psalm --init\' ?' . PHP_EOL;
615
                exit(1);
616
            }
617
            throw new ConfigException('Config not found for path ' . $path);
618
        }
619
620
        return self::loadFromXMLFile($config_path, $current_dir);
621
    }
622
623
    /**
624
     * Searches up a folder hierarchy for the most immediate config.
625
     *
626
     * @throws ConfigException
627
     *
628
     * @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...
629
     */
630
    public static function locateConfigFile(string $path)
631
    {
632
        $dir_path = realpath($path);
633
634
        if ($dir_path === false) {
635
            throw new ConfigException('Config not found for path ' . $path);
636
        }
637
638
        if (!is_dir($dir_path)) {
639
            $dir_path = dirname($dir_path);
640
        }
641
642
        do {
643
            $maybe_path = $dir_path . DIRECTORY_SEPARATOR . Config::DEFAULT_FILE_NAME;
644
645
            if (file_exists($maybe_path) || file_exists($maybe_path .= '.dist')) {
646
                return $maybe_path;
647
            }
648
649
            $dir_path = dirname($dir_path);
650
        } while (dirname($dir_path) !== $dir_path);
651
652
        return null;
653
    }
654
655
    /**
656
     * Creates a new config object from the file
657
     *
658
     * @param  string           $file_path
659
     * @param  string           $current_dir
660
     *
661
     * @return self
662
     */
663
    public static function loadFromXMLFile($file_path, $current_dir)
664
    {
665
        $file_contents = file_get_contents($file_path);
666
667
        $base_dir = dirname($file_path) . DIRECTORY_SEPARATOR;
668
669
        if ($file_contents === false) {
670
            throw new \InvalidArgumentException('Cannot open ' . $file_path);
671
        }
672
673
        try {
674
            $config = self::loadFromXML($base_dir, $file_contents, $current_dir);
675
            $config->hash = sha1($file_contents . \PSALM_VERSION);
676
        } catch (ConfigException $e) {
677
            throw new ConfigException(
678
                'Problem parsing ' . $file_path . ":\n" . '  ' . $e->getMessage()
679
            );
680
        }
681
682
        return $config;
683
    }
684
685
    /**
686
     * Creates a new config object from an XML string
687
     *
688
     * @throws ConfigException
689
     *
690
     * @param  string           $base_dir
691
     * @param  string           $file_contents
692
     * @param  string|null      $current_dir Current working directory, if different to $base_dir
693
     *
694
     * @return self
695
     */
696
    public static function loadFromXML($base_dir, $file_contents, $current_dir = null)
697
    {
698
        if ($current_dir === null) {
699
            $current_dir = $base_dir;
700
        }
701
702
        self::validateXmlConfig($base_dir, $file_contents);
703
704
        return self::fromXmlAndPaths($base_dir, $file_contents, $current_dir);
705
    }
706
707
    private static function loadDomDocument(string $base_dir, string $file_contents): DOMDocument
708
    {
709
        $dom_document = new DOMDocument();
710
711
        // there's no obvious way to set xml:base for a document when loading it from string
712
        // so instead we're changing the current directory instead to be able to process XIncludes
713
        $oldpwd = getcwd();
714
        chdir($base_dir);
715
716
        $dom_document->loadXML($file_contents, LIBXML_NONET);
717
        $dom_document->xinclude(LIBXML_NONET);
718
719
        chdir($oldpwd);
720
        return $dom_document;
721
    }
722
723
    /**
724
     * @throws ConfigException
725
     */
726
    private static function validateXmlConfig(string $base_dir, string $file_contents): void
727
    {
728
        $schema_path = dirname(dirname(__DIR__)) . '/config.xsd';
729
730
        if (!file_exists($schema_path)) {
731
            throw new ConfigException('Cannot locate config schema');
732
        }
733
734
        $dom_document = self::loadDomDocument($base_dir, $file_contents);
735
736
        $psalm_nodes = $dom_document->getElementsByTagName('psalm');
737
738
        /** @var \DomElement|null */
739
        $psalm_node = $psalm_nodes->item(0);
740
741
        if (!$psalm_node) {
742
            throw new ConfigException(
743
                'Missing psalm node'
744
            );
745
        }
746
747
        if (!$psalm_node->hasAttribute('xmlns')) {
748
            $psalm_node->setAttribute('xmlns', 'https://getpsalm.org/schema/config');
749
750
            $old_dom_document = $dom_document;
751
            $dom_document = self::loadDomDocument($base_dir, $old_dom_document->saveXML());
752
        }
753
754
        // Enable user error handling
755
        libxml_use_internal_errors(true);
756
757
        if (!$dom_document->schemaValidate($schema_path)) {
758
            $errors = libxml_get_errors();
759
            foreach ($errors as $error) {
760
                if ($error->level === LIBXML_ERR_FATAL || $error->level === LIBXML_ERR_ERROR) {
761
                    throw new ConfigException(
762
                        'Error on line ' . $error->line . ":\n" . '    ' . $error->message
763
                    );
764
                }
765
            }
766
            libxml_clear_errors();
767
        }
768
    }
769
770
771
    /**
772
     * @psalm-suppress MixedMethodCall
773
     * @psalm-suppress MixedAssignment
774
     * @psalm-suppress MixedOperand
775
     * @psalm-suppress MixedArgument
776
     * @psalm-suppress MixedPropertyFetch
777
     *
778
     * @throws ConfigException
779
     */
780
    private static function fromXmlAndPaths(string $base_dir, string $file_contents, string $current_dir): self
781
    {
782
        $config = new static();
783
784
        $dom_document = self::loadDomDocument($base_dir, $file_contents);
785
786
        $config_xml = simplexml_import_dom($dom_document);
787
788
        $booleanAttributes = [
789
            'useDocblockTypes' => 'use_docblock_types',
790
            'useDocblockPropertyTypes' => 'use_docblock_property_types',
791
            'throwExceptionOnError' => 'throw_exception',
792
            'hideExternalErrors' => 'hide_external_errors',
793
            'resolveFromConfigFile' => 'resolve_from_config_file',
794
            'allowFileIncludes' => 'allow_includes',
795
            'strictBinaryOperands' => 'strict_binary_operands',
796
            'requireVoidReturnType' => 'add_void_docblocks',
797
            'useAssertForType' => 'use_assert_for_type',
798
            'rememberPropertyAssignmentsAfterCall' => 'remember_property_assignments_after_call',
799
            'allowPhpStormGenerics' => 'allow_phpstorm_generics',
800
            'allowStringToStandInForClass' => 'allow_string_standin_for_class',
801
            'usePhpDocMethodsWithoutMagicCall' => 'use_phpdoc_method_without_magic_or_parent',
802
            'usePhpDocPropertiesWithoutMagicCall' => 'use_phpdoc_property_without_magic_or_parent',
803
            'memoizeMethodCallResults' => 'memoize_method_calls',
804
            'hoistConstants' => 'hoist_constants',
805
            'addParamDefaultToDocblockType' => 'add_param_default_to_docblock_type',
806
            'checkForThrowsDocblock' => 'check_for_throws_docblock',
807
            'checkForThrowsInGlobalScope' => 'check_for_throws_in_global_scope',
808
            'forbidEcho' => 'forbid_echo',
809
            'ignoreInternalFunctionFalseReturn' => 'ignore_internal_falsable_issues',
810
            'ignoreInternalFunctionNullReturn' => 'ignore_internal_nullable_issues',
811
            'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline',
812
            'loadXdebugStub' => 'load_xdebug_stub',
813
            'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
814
            'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist',
815
            'reportMixedIssues' => 'show_mixed_issues',
816
            'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes',
817
            'sealAllMethods' => 'seal_all_methods',
818
            'runTaintAnalysis' => 'run_taint_analysis',
819
            'usePhpStormMetaPath' => 'use_phpstorm_meta_path',
820
        ];
821
822
        foreach ($booleanAttributes as $xmlName => $internalName) {
823
            if (isset($config_xml[$xmlName])) {
824
                $attribute_text = (string) $config_xml[$xmlName];
825
                $config->setBooleanAttribute(
826
                    $internalName,
827
                    $attribute_text === 'true' || $attribute_text === '1'
828
                );
829
            }
830
        }
831
832
        if ($config->resolve_from_config_file) {
833
            $config->base_dir = $base_dir;
834
        } else {
835
            $config->base_dir = $current_dir;
836
            $base_dir = $current_dir;
837
        }
838
839
        if (isset($config_xml['phpVersion'])) {
840
            $config->configured_php_version = (string) $config_xml['phpVersion'];
841
        }
842
843
        if (isset($config_xml['autoloader'])) {
844
            $autoloader_path = $config->base_dir . DIRECTORY_SEPARATOR . $config_xml['autoloader'];
845
846
            if (!file_exists($autoloader_path)) {
847
                throw new ConfigException('Cannot locate autoloader');
848
            }
849
850
            $config->autoloader = realpath($autoloader_path);
851
        }
852
853
        if (isset($config_xml['cacheDirectory'])) {
854
            $config->cache_directory = (string)$config_xml['cacheDirectory'];
855
        } else {
856
            $config->cache_directory = sys_get_temp_dir() . '/psalm';
857
        }
858
859
        $config->global_cache_directory = $config->cache_directory;
860
861
        $config->cache_directory .= DIRECTORY_SEPARATOR . sha1($base_dir);
862
863
        if (is_dir($config->cache_directory) === false && @mkdir($config->cache_directory, 0777, true) === false) {
864
            trigger_error('Could not create cache directory: ' . $config->cache_directory, E_USER_ERROR);
865
        }
866
867
        if (isset($config_xml['serializer'])) {
868
            $attribute_text = (string) $config_xml['serializer'];
869
            $config->use_igbinary = $attribute_text === 'igbinary';
870
        } elseif ($igbinary_version = phpversion('igbinary')) {
871
            $config->use_igbinary = version_compare($igbinary_version, '2.0.5') >= 0;
872
        }
873
874
875
        if (isset($config_xml['findUnusedCode'])) {
876
            $attribute_text = (string) $config_xml['findUnusedCode'];
877
            $config->find_unused_code = $attribute_text === 'true' || $attribute_text === '1';
878
            $config->find_unused_variables = $config->find_unused_code;
879
        }
880
881
        if (isset($config_xml['findUnusedVariablesAndParams'])) {
882
            $attribute_text = (string) $config_xml['findUnusedVariablesAndParams'];
883
            $config->find_unused_variables = $attribute_text === 'true' || $attribute_text === '1';
884
        }
885
886
        if (isset($config_xml['errorLevel'])) {
887
            $attribute_text = (int) $config_xml['errorLevel'];
888
889
            if (!in_array($attribute_text, [1, 2, 3, 4, 5, 6, 7, 8], true)) {
890
                throw new Exception\ConfigException(
891
                    'Invalid error level ' . $config_xml['errorLevel']
892
                );
893
            }
894
895
            $config->level = $attribute_text;
896
        } elseif (isset($config_xml['totallyTyped'])) {
897
            $totally_typed = (string) $config_xml['totallyTyped'];
898
899
            if ($totally_typed === 'true' || $totally_typed === '1') {
900
                $config->level = 1;
901
            } else {
902
                $config->level = 2;
903
904
                if ($config->show_mixed_issues === null) {
905
                    $config->show_mixed_issues = false;
906
                }
907
            }
908
        } else {
909
            $config->level = 2;
910
        }
911
912
        if (isset($config_xml['errorBaseline'])) {
913
            $attribute_text = (string) $config_xml['errorBaseline'];
914
            $config->error_baseline = $attribute_text;
915
        }
916
917
        if (isset($config_xml['maxStringLength'])) {
918
            $attribute_text = intval($config_xml['maxStringLength']);
919
            $config->max_string_length = $attribute_text;
920
        }
921
922
        if (isset($config_xml['parseSql'])) {
923
            $attribute_text = (string) $config_xml['parseSql'];
924
            $config->parse_sql = $attribute_text === 'true' || $attribute_text === '1';
925
        }
926
927
        if (isset($config_xml['inferPropertyTypesFromConstructor'])) {
928
            $attribute_text = (string) $config_xml['inferPropertyTypesFromConstructor'];
929
            $config->infer_property_types_from_constructor = $attribute_text === 'true' || $attribute_text === '1';
930
        }
931
932
        if (isset($config_xml->projectFiles)) {
933
            $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true);
934
        }
935
936
        if (isset($config_xml->extraFiles)) {
937
            $config->extra_files = ProjectFileFilter::loadFromXMLElement($config_xml->extraFiles, $base_dir, true);
938
        }
939
940
        if (isset($config_xml->taintAnalysis->ignoreFiles)) {
941
            $config->taint_analysis_ignored_files = TaintAnalysisFileFilter::loadFromXMLElement(
942
                $config_xml->taintAnalysis->ignoreFiles,
943
                $base_dir,
944
                false
945
            );
946
        }
947
948
        if (isset($config_xml->fileExtensions)) {
949
            $config->file_extensions = [];
950
951
            $config->loadFileExtensions($config_xml->fileExtensions->extension);
952
        }
953
954
        if (isset($config_xml->mockClasses) && isset($config_xml->mockClasses->class)) {
955
            /** @var \SimpleXMLElement $mock_class */
956
            foreach ($config_xml->mockClasses->class as $mock_class) {
957
                $config->mock_classes[] = strtolower((string)$mock_class['name']);
958
            }
959
        }
960
961
        if (isset($config_xml->ignoreExceptions)) {
962
            if (isset($config_xml->ignoreExceptions->class)) {
963
                /** @var \SimpleXMLElement $exception_class */
964
                foreach ($config_xml->ignoreExceptions->class as $exception_class) {
965
                    $exception_name = (string) $exception_class['name'];
966
                    $global_attribute_text = (string) $exception_class['onlyGlobalScope'];
967
                    if ($global_attribute_text !== 'true' && $global_attribute_text !== '1') {
968
                        $config->ignored_exceptions[$exception_name] = true;
969
                    }
970
                    $config->ignored_exceptions_in_global_scope[$exception_name] = true;
971
                }
972
            }
973
            if (isset($config_xml->ignoreExceptions->classAndDescendants)) {
974
                /** @var \SimpleXMLElement $exception_class */
975
                foreach ($config_xml->ignoreExceptions->classAndDescendants as $exception_class) {
976
                    $exception_name = (string) $exception_class['name'];
977
                    $global_attribute_text = (string) $exception_class['onlyGlobalScope'];
978
                    if ($global_attribute_text !== 'true' && $global_attribute_text !== '1') {
979
                        $config->ignored_exceptions_and_descendants[$exception_name] = true;
980
                    }
981
                    $config->ignored_exceptions_and_descendants_in_global_scope[$exception_name] = true;
982
                }
983
            }
984
        }
985
986
        if (isset($config_xml->forbiddenFunctions) && isset($config_xml->forbiddenFunctions->function)) {
987
            /** @var \SimpleXMLElement $forbidden_function */
988
            foreach ($config_xml->forbiddenFunctions->function as $forbidden_function) {
989
                $config->forbidden_functions[strtolower((string) $forbidden_function['name'])] = true;
990
            }
991
        }
992
993
        if (isset($config_xml->exitFunctions) && isset($config_xml->exitFunctions->function)) {
994
            /** @var \SimpleXMLElement $exit_function */
995
            foreach ($config_xml->exitFunctions->function as $exit_function) {
996
                $config->exit_functions[strtolower((string) $exit_function['name'])] = true;
997
            }
998
        }
999
1000
        if (isset($config_xml->stubs) && isset($config_xml->stubs->file)) {
1001
            /** @var \SimpleXMLElement $stub_file */
1002
            foreach ($config_xml->stubs->file as $stub_file) {
1003
                $stub_file_name = (string)$stub_file['name'];
1004
                if (!Path::isAbsolute($stub_file_name)) {
1005
                    $stub_file_name = $config->base_dir . DIRECTORY_SEPARATOR . $stub_file_name;
1006
                }
1007
                $file_path = realpath($stub_file_name);
1008
1009
                if (!$file_path) {
1010
                    throw new Exception\ConfigException(
1011
                        'Cannot resolve stubfile path ' . $config->base_dir . DIRECTORY_SEPARATOR . $stub_file['name']
1012
                    );
1013
                }
1014
1015
                $config->addStubFile($file_path);
1016
            }
1017
        }
1018
1019
        // this plugin loading system borrows heavily from etsy/phan
1020
        if (isset($config_xml->plugins)) {
1021
            if (isset($config_xml->plugins->plugin)) {
1022
                /** @var \SimpleXMLElement $plugin */
1023
                foreach ($config_xml->plugins->plugin as $plugin) {
1024
                    $plugin_file_name = (string) $plugin['filename'];
1025
1026
                    $path = Path::isAbsolute($plugin_file_name)
1027
                        ? $plugin_file_name
1028
                        : $config->base_dir . $plugin_file_name;
1029
1030
                    $config->addPluginPath($path);
1031
                }
1032
            }
1033
            if (isset($config_xml->plugins->pluginClass)) {
1034
                /** @var \SimpleXMLElement $plugin */
1035
                foreach ($config_xml->plugins->pluginClass as $plugin) {
1036
                    $plugin_class_name = $plugin['class'];
1037
                    // any child elements are used as plugin configuration
1038
                    $plugin_config = null;
1039
                    if ($plugin->count()) {
1040
                        $plugin_config = $plugin->children();
1041
                    }
1042
1043
                    $config->addPluginClass((string) $plugin_class_name, $plugin_config);
1044
                }
1045
            }
1046
        }
1047
1048
        if (isset($config_xml->issueHandlers)) {
1049
            /** @var \SimpleXMLElement $issue_handler */
1050
            foreach ($config_xml->issueHandlers->children() as $key => $issue_handler) {
1051
                if ($key === 'PluginIssue') {
1052
                    $custom_class_name = (string) $issue_handler['name'];
1053
                    /** @var string $key */
1054
                    $config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement(
1055
                        $issue_handler,
1056
                        $base_dir
1057
                    );
1058
                } else {
1059
                    /** @var string $key */
1060
                    $config->issue_handlers[$key] = IssueHandler::loadFromXMLElement(
1061
                        $issue_handler,
1062
                        $base_dir
1063
                    );
1064
                }
1065
            }
1066
        }
1067
1068
        if (isset($config_xml->globals) && isset($config_xml->globals->var)) {
1069
            /** @var \SimpleXMLElement $var */
1070
            foreach ($config_xml->globals->var as $var) {
1071
                $config->globals['$' . (string) $var['name']] = (string) $var['type'];
1072
            }
1073
        }
1074
1075
        return $config;
1076
    }
1077
1078
    /**
1079
     * @return $this
1080
     */
1081
    public static function getInstance()
1082
    {
1083
        if (self::$instance) {
1084
            return self::$instance;
1085
        }
1086
1087
        throw new \UnexpectedValueException('No config initialized');
1088
    }
1089
1090
    /**
1091
     * @return void
1092
     */
1093
    public function setComposerClassLoader(?ClassLoader $loader = null)
1094
    {
1095
        $this->composer_class_loader = $loader;
1096
    }
1097
1098
    /**
1099
     * @param string $issue_key
1100
     * @param string $error_level
1101
     *
1102
     * @return void
1103
     */
1104
    public function setCustomErrorLevel($issue_key, $error_level)
1105
    {
1106
        $this->issue_handlers[$issue_key] = new IssueHandler();
1107
        $this->issue_handlers[$issue_key]->setErrorLevel($error_level);
1108
    }
1109
1110
    /**
1111
     * @param  array<SimpleXMLElement> $extensions
1112
     *
1113
     * @throws ConfigException if a Config file could not be found
1114
     *
1115
     * @return void
1116
     */
1117
    private function loadFileExtensions($extensions)
1118
    {
1119
        foreach ($extensions as $extension) {
1120
            $extension_name = preg_replace('/^\.?/', '', (string)$extension['name']);
1121
            $this->file_extensions[] = $extension_name;
1122
1123
            if (isset($extension['scanner'])) {
1124
                $path = $this->base_dir . (string)$extension['scanner'];
1125
1126
                if (!file_exists($path)) {
1127
                    throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
1128
                }
1129
1130
                $this->filetype_scanner_paths[$extension_name] = $path;
1131
            }
1132
1133
            if (isset($extension['checker'])) {
1134
                $path = $this->base_dir . (string)$extension['checker'];
1135
1136
                if (!file_exists($path)) {
1137
                    throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
1138
                }
1139
1140
                $this->filetype_analyzer_paths[$extension_name] = $path;
1141
            }
1142
        }
1143
    }
1144
1145
    /**
1146
     * @param string $path
1147
     *
1148
     * @return void
1149
     */
1150
    public function addPluginPath($path)
1151
    {
1152
        if (!file_exists($path)) {
1153
            throw new \InvalidArgumentException('Cannot find plugin file ' . $path);
1154
        }
1155
1156
        $this->plugin_paths[] = $path;
1157
    }
1158
1159
    /** @return void */
1160
    public function addPluginClass(string $class_name, SimpleXMLElement $plugin_config = null)
1161
    {
1162
        $this->plugin_classes[] = ['class' => $class_name, 'config' => $plugin_config];
1163
    }
1164
1165
    /** @return array<array{class:string, config:?SimpleXmlElement}> */
0 ignored issues
show
The doc-type array<array{class:string, could not be parsed: Unknown type name "array{class:string" at position 6. (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...
1166
    public function getPluginClasses(): array
1167
    {
1168
        return $this->plugin_classes;
1169
    }
1170
1171
    /**
1172
     * Initialises all the plugins (done once the config is fully loaded)
1173
     *
1174
     * @return void
1175
     * @psalm-suppress MixedAssignment
1176
     * @psalm-suppress MixedTypeCoercion
1177
     */
1178
    public function initializePlugins(ProjectAnalyzer $project_analyzer)
1179
    {
1180
        $codebase = $project_analyzer->getCodebase();
1181
1182
        $project_analyzer->progress->debug('Initializing plugins...' . PHP_EOL);
1183
1184
        $socket = new PluginRegistrationSocket($this, $codebase);
1185
        // initialize plugin classes earlier to let them hook into subsequent load process
1186
        foreach ($this->plugin_classes as $plugin_class_entry) {
1187
            $plugin_class_name = $plugin_class_entry['class'];
1188
            $plugin_config = $plugin_class_entry['config'];
1189
1190
            try {
1191
                // Below will attempt to load plugins from the project directory first.
1192
                // Failing that, it will use registered autoload chain, which will load
1193
                // plugins from Psalm directory or phar file. If that fails as well, it
1194
                // will fall back to project autoloader. It may seem that the last step
1195
                // will always fail, but it's only true if project uses Composer autoloader
1196
                if ($this->composer_class_loader
1197
                    && ($plugin_class_path = $this->composer_class_loader->findFile($plugin_class_name))
1198
                ) {
1199
                    $project_analyzer->progress->debug(
1200
                        'Loading plugin ' . $plugin_class_name . ' via require'. PHP_EOL
1201
                    );
1202
1203
                    /** @psalm-suppress UnresolvableInclude */
1204
                    require_once($plugin_class_path);
1205
                } else {
1206
                    if (!class_exists($plugin_class_name, true)) {
1207
                        throw new \UnexpectedValueException($plugin_class_name . ' is not a known class');
1208
                    }
1209
                }
1210
1211
                /**
1212
                 * @psalm-suppress InvalidStringClass
1213
                 *
1214
                 * @var Plugin\PluginEntryPointInterface
1215
                 */
1216
                $plugin_object = new $plugin_class_name;
1217
                $plugin_object($socket, $plugin_config);
1218
            } catch (\Throwable $e) {
1219
                throw new ConfigException('Failed to load plugin ' . $plugin_class_name, 0, $e);
1220
            }
1221
1222
            $project_analyzer->progress->debug('Loaded plugin ' . $plugin_class_name . ' successfully'. PHP_EOL);
1223
        }
1224
1225
        foreach ($this->filetype_scanner_paths as $extension => $path) {
1226
            $fq_class_name = $this->getPluginClassForPath(
1227
                $codebase,
1228
                $path,
1229
                FileScanner::class
1230
            );
1231
1232
            /** @psalm-suppress UnresolvableInclude */
1233
            require_once($path);
1234
1235
            $this->filetype_scanners[$extension] = $fq_class_name;
1236
        }
1237
1238
        foreach ($this->filetype_analyzer_paths as $extension => $path) {
1239
            $fq_class_name = $this->getPluginClassForPath(
1240
                $codebase,
1241
                $path,
1242
                FileAnalyzer::class
1243
            );
1244
1245
            /** @psalm-suppress UnresolvableInclude */
1246
            require_once($path);
1247
1248
            $this->filetype_analyzers[$extension] = $fq_class_name;
1249
        }
1250
1251
        foreach ($this->plugin_paths as $path) {
1252
            try {
1253
                $plugin_object = new FileBasedPluginAdapter($path, $this, $codebase);
1254
                $plugin_object($socket);
1255
            } catch (\Throwable $e) {
1256
                throw new ConfigException('Failed to load plugin ' . $path, 0, $e);
1257
            }
1258
        }
1259
    }
1260
1261
    /**
1262
     * @template T
1263
     *
1264
     * @param  string $path
1265
     * @param  T::class $must_extend
0 ignored issues
show
The doc-type T::class could not be parsed: Unknown type name "T::class" at position 0. (view supported doc-types)

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

Loading history...
1266
     *
1267
     * @return class-string<T>
0 ignored issues
show
The doc-type class-string<T> could not be parsed: Unknown type name "class-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...
1268
     */
1269
    private function getPluginClassForPath(Codebase $codebase, $path, $must_extend)
1270
    {
1271
        $file_storage = $codebase->createFileStorageForPath($path);
1272
        $file_to_scan = new FileScanner($path, $this->shortenFileName($path), true);
1273
        $file_to_scan->scan(
1274
            $codebase,
1275
            $file_storage
1276
        );
1277
1278
        $declared_classes = ClassLikeAnalyzer::getClassesForFile($codebase, $path);
1279
1280
        if (!count($declared_classes)) {
1281
            throw new \InvalidArgumentException(
1282
                'Plugins must have at least one class in the file - ' . $path . ' has ' .
1283
                    count($declared_classes)
1284
            );
1285
        }
1286
1287
        $fq_class_name = reset($declared_classes);
1288
1289
        if (!$codebase->classlikes->classExtends(
1290
            $fq_class_name,
1291
            $must_extend
1292
        )
1293
        ) {
1294
            throw new \InvalidArgumentException(
1295
                'This plugin must extend ' . $must_extend . ' - ' . $path . ' does not'
1296
            );
1297
        }
1298
1299
        /**
1300
         * @var class-string<T>
1301
         */
1302
        return $fq_class_name;
1303
    }
1304
1305
    /**
1306
     * @param  string $file_name
1307
     *
1308
     * @return string
1309
     */
1310
    public function shortenFileName($file_name)
1311
    {
1312
        return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $file_name);
1313
    }
1314
1315
    /**
1316
     * @param   string $issue_type
1317
     * @param   string $file_path
1318
     *
1319
     * @return  bool
1320
     */
1321
    public function reportIssueInFile($issue_type, $file_path)
1322
    {
1323
        if (($this->show_mixed_issues === false || $this->level > 2)
1324
            && in_array($issue_type, self::MIXED_ISSUES, true)
1325
        ) {
1326
            return false;
1327
        }
1328
1329
        if ($this->mustBeIgnored($file_path)) {
1330
            return false;
1331
        }
1332
1333
        $dependent_files = [strtolower($file_path) => $file_path];
1334
1335
        $project_analyzer = ProjectAnalyzer::getInstance();
1336
1337
        $codebase = $project_analyzer->getCodebase();
1338
1339
        if (!$this->hide_external_errors) {
1340
            try {
1341
                $file_storage = $codebase->file_storage_provider->get($file_path);
1342
                $dependent_files += $file_storage->required_by_file_paths;
1343
            } catch (\InvalidArgumentException $e) {
1344
                // do nothing
1345
            }
1346
        }
1347
1348
        $any_file_path_matched = false;
1349
1350
        foreach ($dependent_files as $dependent_file_path) {
1351
            if (((!$project_analyzer->full_run && $codebase->analyzer->canReportIssues($dependent_file_path))
1352
                    || $project_analyzer->canReportIssues($dependent_file_path))
1353
                && ($file_path === $dependent_file_path || !$this->mustBeIgnored($dependent_file_path))
1354
            ) {
1355
                $any_file_path_matched = true;
1356
                break;
1357
            }
1358
        }
1359
1360
        if (!$any_file_path_matched) {
1361
            return false;
1362
        }
1363
1364
        if ($this->getReportingLevelForFile($issue_type, $file_path) === self::REPORT_SUPPRESS) {
1365
            return false;
1366
        }
1367
1368
        return true;
1369
    }
1370
1371
    /**
1372
     * @param   string $file_path
1373
     *
1374
     * @return  bool
1375
     */
1376
    public function isInProjectDirs($file_path)
1377
    {
1378
        return $this->project_files && $this->project_files->allows($file_path);
1379
    }
1380
1381
    /**
1382
     * @param   string $file_path
1383
     *
1384
     * @return  bool
1385
     */
1386
    public function isInExtraDirs($file_path)
1387
    {
1388
        return $this->extra_files && $this->extra_files->allows($file_path);
1389
    }
1390
1391
    /**
1392
     * @param   string $file_path
1393
     *
1394
     * @return  bool
1395
     */
1396
    public function mustBeIgnored($file_path)
1397
    {
1398
        return $this->project_files && $this->project_files->forbids($file_path);
1399
    }
1400
1401
    public function trackTaintsInPath(string $file_path) : bool
1402
    {
1403
        return !$this->taint_analysis_ignored_files
1404
            || $this->taint_analysis_ignored_files->allows($file_path);
1405
    }
1406
1407
    public function getReportingLevelForIssue(CodeIssue $e) : string
1408
    {
1409
        $fqcn_parts = explode('\\', get_class($e));
1410
        $issue_type = array_pop($fqcn_parts);
1411
1412
        $reporting_level = null;
1413
1414
        if ($e instanceof ClassIssue) {
1415
            $reporting_level = $this->getReportingLevelForClass($issue_type, $e->fq_classlike_name);
1416
        } elseif ($e instanceof MethodIssue) {
1417
            $reporting_level = $this->getReportingLevelForMethod($issue_type, $e->method_id);
1418
        } elseif ($e instanceof FunctionIssue) {
1419
            $reporting_level = $this->getReportingLevelForFunction($issue_type, $e->function_id);
1420
        } elseif ($e instanceof PropertyIssue) {
1421
            $reporting_level = $this->getReportingLevelForProperty($issue_type, $e->property_id);
1422
        } elseif ($e instanceof ArgumentIssue && $e->function_id) {
1423
            $reporting_level = $this->getReportingLevelForArgument($issue_type, $e->function_id);
1424
        } elseif ($e instanceof VariableIssue) {
1425
            $reporting_level = $this->getReportingLevelForVariable($issue_type, $e->var_name);
1426
        }
1427
1428
        if ($reporting_level === null) {
1429
            $reporting_level = $this->getReportingLevelForFile($issue_type, $e->getFilePath());
1430
        }
1431
1432
        $parent_issue_type = self::getParentIssueType($issue_type);
1433
1434
        if ($parent_issue_type && $reporting_level === Config::REPORT_ERROR) {
1435
            $parent_reporting_level = $this->getReportingLevelForFile($parent_issue_type, $e->getFilePath());
1436
1437
            if ($parent_reporting_level !== $reporting_level) {
1438
                return $parent_reporting_level;
1439
            }
1440
        }
1441
1442
        return $reporting_level;
1443
    }
1444
1445
    /**
1446
     * @param  string $issue_type
1447
     *
1448
     * @return string|null
1449
     */
1450
    public static function getParentIssueType($issue_type)
1451
    {
1452
        if ($issue_type === 'PossiblyUndefinedIntArrayOffset'
1453
            || $issue_type === 'PossiblyUndefinedStringArrayOffset'
1454
        ) {
1455
            return 'PossiblyUndefinedArrayOffset';
1456
        }
1457
1458
        if ($issue_type === 'PossiblyNullReference') {
1459
            return 'NullReference';
1460
        }
1461
1462
        if ($issue_type === 'PossiblyFalseReference') {
1463
            return null;
1464
        }
1465
1466
        if ($issue_type === 'PossiblyUndefinedArrayOffset') {
1467
            return null;
1468
        }
1469
1470
        if (strpos($issue_type, 'Possibly') === 0) {
1471
            $stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type);
1472
1473
            if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) {
1474
                $stripped_issue_type = 'Invalid' . $stripped_issue_type;
1475
            }
1476
1477
            return $stripped_issue_type;
1478
        }
1479
1480
        if (preg_match('/^(False|Null)[A-Z]/', $issue_type) && !strpos($issue_type, 'Reference')) {
1481
            return preg_replace('/^(False|Null)/', 'Invalid', $issue_type);
1482
        }
1483
1484
        if ($issue_type === 'UndefinedInterfaceMethod') {
1485
            return 'UndefinedMethod';
1486
        }
1487
1488
        if ($issue_type === 'UndefinedMagicPropertyFetch') {
1489
            return 'UndefinedPropertyFetch';
1490
        }
1491
1492
        if ($issue_type === 'UndefinedMagicPropertyAssignment') {
1493
            return 'UndefinedPropertyAssignment';
1494
        }
1495
1496
        if ($issue_type === 'UndefinedMagicMethod') {
1497
            return 'UndefinedMethod';
1498
        }
1499
1500
        if ($issue_type === 'PossibleRawObjectIteration') {
1501
            return 'RawObjectIteration';
1502
        }
1503
1504
        if ($issue_type === 'UninitializedProperty') {
1505
            return 'PropertyNotSetInConstructor';
1506
        }
1507
1508
        if ($issue_type === 'InvalidDocblockParamName') {
1509
            return 'InvalidDocblock';
1510
        }
1511
1512
        if ($issue_type === 'UnusedClosureParam') {
1513
            return 'UnusedParam';
1514
        }
1515
1516
        if ($issue_type === 'StringIncrement') {
1517
            return 'InvalidOperand';
1518
        }
1519
1520
        if ($issue_type === 'TraitMethodSignatureMismatch') {
1521
            return 'MethodSignatureMismatch';
1522
        }
1523
1524
        if ($issue_type === 'ImplementedParamTypeMismatch') {
1525
            return 'MoreSpecificImplementedParamType';
1526
        }
1527
1528
        if ($issue_type === 'UndefinedDocblockClass') {
1529
            return 'UndefinedClass';
1530
        }
1531
1532
        if ($issue_type === 'MixedArgumentTypeCoercion'
1533
            || $issue_type === 'MixedPropertyTypeCoercion'
1534
            || $issue_type === 'MixedReturnTypeCoercion'
1535
            || $issue_type === 'MixedArrayTypeCoercion'
1536
        ) {
1537
            return 'MixedTypeCoercion';
1538
        }
1539
1540
        if ($issue_type === 'ArgumentTypeCoercion'
1541
            || $issue_type === 'PropertyTypeCoercion'
1542
            || $issue_type === 'ReturnTypeCoercion'
1543
        ) {
1544
            return 'TypeCoercion';
1545
        }
1546
1547
        return null;
1548
    }
1549
1550
    /**
1551
     * @param   string $issue_type
1552
     * @param   string $file_path
1553
     *
1554
     * @return  string
1555
     */
1556
    public function getReportingLevelForFile($issue_type, $file_path)
1557
    {
1558
        if (isset($this->issue_handlers[$issue_type])) {
1559
            return $this->issue_handlers[$issue_type]->getReportingLevelForFile($file_path);
1560
        }
1561
1562
        // this string is replaced by scoper for Phars, so be careful
1563
        $issue_class = 'Psalm\\Issue\\' . $issue_type;
1564
1565
        if (!class_exists($issue_class) || !is_a($issue_class, \Psalm\Issue\CodeIssue::class, true)) {
1566
            return self::REPORT_ERROR;
1567
        }
1568
1569
        /** @var int */
1570
        $issue_level = $issue_class::ERROR_LEVEL;
1571
1572
        if ($issue_level > 0 && $issue_level < $this->level) {
1573
            return self::REPORT_INFO;
1574
        }
1575
1576
        return self::REPORT_ERROR;
1577
    }
1578
1579
    /**
1580
     * @param   string $issue_type
1581
     * @param   string $fq_classlike_name
1582
     *
1583
     * @return  string|null
1584
     */
1585
    public function getReportingLevelForClass($issue_type, $fq_classlike_name)
1586
    {
1587
        if (isset($this->issue_handlers[$issue_type])) {
1588
            return $this->issue_handlers[$issue_type]->getReportingLevelForClass($fq_classlike_name);
1589
        }
1590
    }
1591
1592
    /**
1593
     * @param   string $issue_type
1594
     * @param   string $method_id
1595
     *
1596
     * @return  string|null
1597
     */
1598
    public function getReportingLevelForMethod($issue_type, $method_id)
1599
    {
1600
        if (isset($this->issue_handlers[$issue_type])) {
1601
            return $this->issue_handlers[$issue_type]->getReportingLevelForMethod($method_id);
1602
        }
1603
    }
1604
1605
    /**
1606
     * @return  string|null
1607
     */
1608
    public function getReportingLevelForFunction(string $issue_type, string $function_id)
1609
    {
1610
        if (isset($this->issue_handlers[$issue_type])) {
1611
            return $this->issue_handlers[$issue_type]->getReportingLevelForFunction($function_id);
1612
        }
1613
    }
1614
1615
    /**
1616
     * @return  string|null
1617
     */
1618
    public function getReportingLevelForArgument(string $issue_type, string $function_id)
1619
    {
1620
        if (isset($this->issue_handlers[$issue_type])) {
1621
            return $this->issue_handlers[$issue_type]->getReportingLevelForArgument($function_id);
1622
        }
1623
    }
1624
1625
    /**
1626
     * @param   string $issue_type
1627
     * @param   string $property_id
1628
     *
1629
     * @return  string|null
1630
     */
1631
    public function getReportingLevelForProperty($issue_type, $property_id)
1632
    {
1633
        if (isset($this->issue_handlers[$issue_type])) {
1634
            return $this->issue_handlers[$issue_type]->getReportingLevelForProperty($property_id);
1635
        }
1636
    }
1637
1638
    /**
1639
     * @param   string $issue_type
1640
     * @param   string $var_name
1641
     *
1642
     * @return  string|null
1643
     */
1644
    public function getReportingLevelForVariable(string $issue_type, string $var_name)
1645
    {
1646
        if (isset($this->issue_handlers[$issue_type])) {
1647
            return $this->issue_handlers[$issue_type]->getReportingLevelForVariable($var_name);
1648
        }
1649
    }
1650
1651
    /**
1652
     * @return array<string>
1653
     */
1654
    public function getProjectDirectories()
1655
    {
1656
        if (!$this->project_files) {
1657
            return [];
1658
        }
1659
1660
        return $this->project_files->getDirectories();
1661
    }
1662
1663
    /**
1664
     * @return array<string>
1665
     */
1666
    public function getProjectFiles()
1667
    {
1668
        if (!$this->project_files) {
1669
            return [];
1670
        }
1671
1672
        return $this->project_files->getFiles();
1673
    }
1674
1675
    /**
1676
     * @return array<string>
1677
     */
1678
    public function getExtraDirectories()
1679
    {
1680
        if (!$this->extra_files) {
1681
            return [];
1682
        }
1683
1684
        return $this->extra_files->getDirectories();
1685
    }
1686
1687
    /**
1688
     * @param   string $file_path
1689
     *
1690
     * @return  bool
1691
     */
1692
    public function reportTypeStatsForFile($file_path)
1693
    {
1694
        return $this->project_files
1695
            && $this->project_files->allows($file_path)
1696
            && $this->project_files->reportTypeStats($file_path);
1697
    }
1698
1699
    /**
1700
     * @param   string $file_path
1701
     *
1702
     * @return  bool
1703
     */
1704
    public function useStrictTypesForFile($file_path)
1705
    {
1706
        return $this->project_files && $this->project_files->useStrictTypes($file_path);
1707
    }
1708
1709
    /**
1710
     * @return array<string>
1711
     */
1712
    public function getFileExtensions()
1713
    {
1714
        return $this->file_extensions;
1715
    }
1716
1717
    /**
1718
     * @return array<string, class-string<FileScanner>>
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...
1719
     */
1720
    public function getFiletypeScanners()
1721
    {
1722
        return $this->filetype_scanners;
1723
    }
1724
1725
    /**
1726
     * @return array<string, class-string<FileAnalyzer>>
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...
1727
     */
1728
    public function getFiletypeAnalyzers()
1729
    {
1730
        return $this->filetype_analyzers;
1731
    }
1732
1733
    /**
1734
     * @return array<int, string>
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...
1735
     */
1736
    public function getMockClasses()
1737
    {
1738
        return $this->mock_classes;
1739
    }
1740
1741
    /**
1742
     * @return void
1743
     */
1744
    public function visitStubFiles(Codebase $codebase, Progress $progress = null)
1745
    {
1746
        if ($progress === null) {
1747
            $progress = new VoidProgress();
1748
        }
1749
1750
        $codebase->register_stub_files = true;
1751
1752
        // note: don't realpath $generic_stubs_path, or phar version will fail
1753
        $generic_stubs_path = __DIR__ . '/Internal/Stubs/CoreGenericFunctions.phpstub';
1754
1755
        if (!file_exists($generic_stubs_path)) {
1756
            throw new \UnexpectedValueException('Cannot locate core generic stubs');
1757
        }
1758
1759
        // note: don't realpath $generic_classes_path, or phar version will fail
1760
        $generic_classes_path = __DIR__ . '/Internal/Stubs/CoreGenericClasses.phpstub';
1761
1762
        if (!file_exists($generic_classes_path)) {
1763
            throw new \UnexpectedValueException('Cannot locate core generic classes');
1764
        }
1765
1766
        // note: don't realpath $generic_classes_path, or phar version will fail
1767
        $immutable_classes_path = __DIR__ . '/Internal/Stubs/CoreImmutableClasses.phpstub';
1768
1769
        if (!file_exists($immutable_classes_path)) {
1770
            throw new \UnexpectedValueException('Cannot locate core immutable classes');
1771
        }
1772
1773
        $core_generic_files = [$generic_stubs_path, $generic_classes_path, $immutable_classes_path];
1774
1775
        if (\extension_loaded('ds')) {
1776
            $ext_ds_path = __DIR__ . '/Internal/Stubs/ext-ds.php';
1777
1778
            if (!file_exists($ext_ds_path)) {
1779
                throw new \UnexpectedValueException('Cannot locate core generic classes');
1780
            }
1781
1782
            $core_generic_files[] = $ext_ds_path;
1783
        }
1784
1785
        $stub_files = array_merge($core_generic_files, $this->stub_files);
1786
1787
        $phpstorm_meta_path = $this->base_dir . DIRECTORY_SEPARATOR . '.phpstorm.meta.php';
1788
1789
        if ($this->use_phpstorm_meta_path) {
1790
            if (is_file($phpstorm_meta_path)) {
1791
                $stub_files[] = $phpstorm_meta_path;
1792
            } elseif (is_dir($phpstorm_meta_path)) {
1793
                $phpstorm_meta_path = realpath($phpstorm_meta_path);
1794
1795
                foreach (glob($phpstorm_meta_path . '/*.meta.php', GLOB_NOSORT) as $glob) {
1796
                    if (is_file($glob) && realpath(dirname($glob)) === $phpstorm_meta_path) {
1797
                        $stub_files[] = $glob;
1798
                    }
1799
                }
1800
            }
1801
        }
1802
1803
        if ($this->load_xdebug_stub) {
1804
            $xdebug_stub_path = __DIR__ . '/Internal/Stubs/Xdebug.php';
1805
1806
            if (!file_exists($xdebug_stub_path)) {
1807
                throw new \UnexpectedValueException('Cannot locate XDebug stub');
1808
            }
1809
1810
            $stub_files[] = $xdebug_stub_path;
1811
        }
1812
1813
        foreach ($stub_files as $file_path) {
1814
            $file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
1815
            $codebase->scanner->addFileToDeepScan($file_path);
1816
        }
1817
1818
        $progress->debug('Registering stub files' . "\n");
1819
1820
        $codebase->scanFiles();
1821
1822
        $progress->debug('Finished registering stub files' . "\n");
1823
1824
        $codebase->register_stub_files = false;
1825
    }
1826
1827
    /**
1828
     * @return string
1829
     */
1830
    public function getCacheDirectory()
1831
    {
1832
        return $this->cache_directory;
1833
    }
1834
1835
    /**
1836
     * @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...
1837
     */
1838
    public function getGlobalCacheDirectory()
1839
    {
1840
        return $this->global_cache_directory;
1841
    }
1842
1843
    /**
1844
     * @return array<string, mixed>
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...
1845
     */
1846
    public function getPredefinedConstants()
1847
    {
1848
        return $this->predefined_constants;
1849
    }
1850
1851
    /**
1852
     * @return void
1853
     */
1854
    public function collectPredefinedConstants()
1855
    {
1856
        $this->predefined_constants = get_defined_constants();
1857
    }
1858
1859
    /**
1860
     * @return array<callable-string, bool>
0 ignored issues
show
The doc-type array<callable-string, could not be parsed: Unknown type name "callable-string" at position 6. (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...
1861
     */
1862
    public function getPredefinedFunctions()
1863
    {
1864
        return $this->predefined_functions;
1865
    }
1866
1867
    /**
1868
     * @return void
1869
     */
1870
    public function collectPredefinedFunctions()
1871
    {
1872
        $defined_functions = get_defined_functions();
1873
1874
        if (isset($defined_functions['user'])) {
1875
            foreach ($defined_functions['user'] as $function_name) {
1876
                $this->predefined_functions[$function_name] = true;
1877
            }
1878
        }
1879
1880
        if (isset($defined_functions['internal'])) {
1881
            foreach ($defined_functions['internal'] as $function_name) {
1882
                $this->predefined_functions[$function_name] = true;
1883
            }
1884
        }
1885
    }
1886
1887
    public function setIncludeCollector(IncludeCollector $include_collector): void
1888
    {
1889
        $this->include_collector = $include_collector;
1890
    }
1891
1892
    /**
1893
     * @return void
1894
     *
1895
     * @psalm-suppress MixedAssignment
1896
     * @psalm-suppress MixedArrayAccess
1897
     */
1898
    public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, Progress $progress = null)
1899
    {
1900
        if ($progress === null) {
1901
            $progress = new VoidProgress();
1902
        }
1903
1904
        if (!$this->include_collector) {
1905
            throw new LogicException("IncludeCollector should be set at this point");
1906
        }
1907
1908
        $this->collectPredefinedConstants();
1909
        $this->collectPredefinedFunctions();
1910
1911
        $vendor_autoload_files_path
1912
            = $this->base_dir . DIRECTORY_SEPARATOR . 'vendor'
1913
                . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_files.php';
1914
1915
        if (file_exists($vendor_autoload_files_path)) {
1916
            $this->include_collector->runAndCollect(
1917
                function () use ($vendor_autoload_files_path) {
1918
                    /**
1919
                     * @psalm-suppress UnresolvableInclude
1920
                     * @var string[]
1921
                     */
1922
                    return require $vendor_autoload_files_path;
1923
                }
1924
            );
1925
        }
1926
1927
        $codebase = $project_analyzer->getCodebase();
1928
1929
        if ($this->autoloader) {
1930
            // somee classes that we think are missing may not actually be missing
1931
            // as they might be autoloadable once we require the autoloader below
1932
            $codebase->classlikes->forgetMissingClassLikes();
1933
1934
            $this->include_collector->runAndCollect(
1935
                function () {
1936
                    // do this in a separate method so scope does not leak
1937
                    /** @psalm-suppress UnresolvableInclude */
1938
                    require $this->autoloader;
1939
                }
1940
            );
1941
        }
1942
1943
        $autoload_included_files = $this->include_collector->getFilteredIncludedFiles();
1944
1945
        if ($autoload_included_files) {
1946
            $codebase->register_autoload_files = true;
1947
1948
            $progress->debug('Registering autoloaded files' . "\n");
1949
            foreach ($autoload_included_files as $file_path) {
1950
                $file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
1951
                $progress->debug('   ' . $file_path . "\n");
1952
                $codebase->scanner->addFileToDeepScan($file_path);
1953
            }
1954
1955
            $codebase->scanner->scanFiles($codebase->classlikes);
1956
1957
            $progress->debug('Finished registering autoloaded files' . "\n");
1958
1959
            $codebase->register_autoload_files = false;
1960
        }
1961
    }
1962
1963
    /**
1964
     * @param  string $fq_classlike_name
1965
     *
1966
     * @return string|false
1967
     */
1968
    public function getComposerFilePathForClassLike($fq_classlike_name)
1969
    {
1970
        if (!$this->composer_class_loader) {
1971
            return false;
1972
        }
1973
1974
        return $this->composer_class_loader->findFile($fq_classlike_name);
1975
    }
1976
1977
    public function getPotentialComposerFilePathForClassLike(string $class) : ?string
1978
    {
1979
        if (!$this->composer_class_loader) {
1980
            return null;
1981
        }
1982
1983
        /** @var array<string, array<int, string>> */
1984
        $psr4_prefixes = $this->composer_class_loader->getPrefixesPsr4();
1985
1986
        // PSR-4 lookup
1987
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';
1988
1989
        $candidate_path = null;
1990
1991
        $maxDepth = 0;
1992
1993
        $subPath = $class;
1994
        while (false !== $lastPos = strrpos($subPath, '\\')) {
1995
            $subPath = substr($subPath, 0, $lastPos);
1996
            $search = $subPath . '\\';
1997
            if (isset($psr4_prefixes[$search])) {
1998
                $depth = substr_count($search, '\\');
1999
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
2000
2001
                foreach ($psr4_prefixes[$search] as $dir) {
2002
                    $dir = realpath($dir);
2003
2004
                    if ($dir
2005
                        && $depth > $maxDepth
2006
                        && $this->isInProjectDirs($dir . DIRECTORY_SEPARATOR . 'testdummy.php')
2007
                    ) {
2008
                        $maxDepth = $depth;
2009
                        $candidate_path = realpath($dir) . $pathEnd;
2010
                    }
2011
                }
2012
            }
2013
        }
2014
2015
        return $candidate_path;
2016
    }
2017
2018
    /**
2019
     * @param string $dir
2020
     *
2021
     * @return void
2022
     */
2023
    public static function removeCacheDirectory($dir)
2024
    {
2025
        if (is_dir($dir)) {
2026
            $objects = scandir($dir, SCANDIR_SORT_NONE);
2027
2028
            if ($objects === false) {
2029
                throw new \UnexpectedValueException('Not expecting false here');
2030
            }
2031
2032
            foreach ($objects as $object) {
2033
                if ($object != '.' && $object != '..') {
2034
                    if (filetype($dir . '/' . $object) == 'dir') {
2035
                        self::removeCacheDirectory($dir . '/' . $object);
2036
                    } else {
2037
                        unlink($dir . '/' . $object);
2038
                    }
2039
                }
2040
            }
2041
2042
            reset($objects);
2043
            rmdir($dir);
2044
        }
2045
    }
2046
2047
    /**
2048
     * @return void
2049
     */
2050
    public function setServerMode()
2051
    {
2052
        $this->cache_directory .= '-s';
2053
    }
2054
2055
    /** @return void */
2056
    public function addStubFile(string $stub_file)
2057
    {
2058
        $this->stub_files[$stub_file] = $stub_file;
2059
    }
2060
2061
    public function hasStubFile(string $stub_file) : bool
2062
    {
2063
        return isset($this->stub_files[$stub_file]);
2064
    }
2065
2066
    /**
2067
     * @return array<string, string>
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...
2068
     */
2069
    public function getStubFiles(): array
2070
    {
2071
        return $this->stub_files;
2072
    }
2073
2074
    public function getPhpVersion(): ?string
2075
    {
2076
        if (isset($this->configured_php_version)) {
2077
            return $this->configured_php_version;
2078
        }
2079
2080
        return $this->getPHPVersionFromComposerJson();
2081
    }
2082
2083
    private function setBooleanAttribute(string $name, bool $value): void
2084
    {
2085
        $this->$name = $value;
2086
    }
2087
2088
    /**
2089
     * @psalm-suppress MixedAssignment
2090
     * @psalm-suppress MixedArrayAccess
2091
     */
2092
    private function getPHPVersionFromComposerJson(): ?string
2093
    {
2094
        $composer_json_path = $this->base_dir . DIRECTORY_SEPARATOR. 'composer.json';
2095
2096
        if (file_exists($composer_json_path)) {
2097
            if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
2098
                throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path);
2099
            }
2100
            $php_version = $composer_json['require']['php'] ?? null;
2101
2102
            if (\is_string($php_version)) {
2103
                foreach (['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0'] as $candidate) {
2104
                    if (Semver::satisfies($candidate, $php_version)) {
2105
                        return $candidate;
2106
                    }
2107
                }
2108
            }
2109
        }
2110
        return null;
2111
    }
2112
}
2113