Test Setup Failed
Push — master ( 8fbc8d...aaba3a )
by Matthew
05:34
created

Config::getReportingLevelForVariable()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 6
rs 10
c 0
b 0
f 0
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
    /**
410
     * Whether to resolve file and directory paths from the location of the config file,
411
     * instead of the current working directory.
412
     *
413
     * @var bool
414
     */
415
    public $resolve_from_config_file = false;
416
417
    /**
418
     * @var string[]
419
     */
420
    public $plugin_paths = [];
421
422
    /**
423
     * @var array<array{class:string,config:?SimpleXMLElement}>
424
     */
425
    private $plugin_classes = [];
426
427
    /**
428
     * Static methods to be called after method checks have completed
429
     *
430
     * @var class-string<Hook\AfterMethodCallAnalysisInterface>[]
431
     */
432
    public $after_method_checks = [];
433
434
    /**
435
     * Static methods to be called after project function checks have completed
436
     *
437
     * Called after function calls to functions defined in the project.
438
     *
439
     * Allows influencing the return type and adding of modifications.
440
     *
441
     * @var class-string<Hook\AfterFunctionCallAnalysisInterface>[]
442
     */
443
    public $after_function_checks = [];
444
445
    /**
446
     * Static methods to be called after every function call
447
     *
448
     * Called after each function call, including php internal functions.
449
     *
450
     * Cannot change the call or influence its return type
451
     *
452
     * @var class-string<Hook\AfterEveryFunctionCallAnalysisInterface>[]
453
     */
454
    public $after_every_function_checks = [];
455
456
457
    /**
458
     * Static methods to be called after expression checks have completed
459
     *
460
     * @var class-string<Hook\AfterExpressionAnalysisInterface>[]
461
     */
462
    public $after_expression_checks = [];
463
464
    /**
465
     * Static methods to be called after statement checks have completed
466
     *
467
     * @var class-string<Hook\AfterStatementAnalysisInterface>[]
468
     */
469
    public $after_statement_checks = [];
470
471
    /**
472
     * Static methods to be called after method checks have completed
473
     *
474
     * @var class-string<Hook\StringInterpreterInterface>[]
475
     */
476
    public $string_interpreters = [];
477
478
    /**
479
     * Static methods to be called after classlike exists checks have completed
480
     *
481
     * @var class-string<Hook\AfterClassLikeExistenceCheckInterface>[]
482
     */
483
    public $after_classlike_exists_checks = [];
484
485
    /**
486
     * Static methods to be called after classlike checks have completed
487
     *
488
     * @var class-string<Hook\AfterClassLikeAnalysisInterface>[]
489
     */
490
    public $after_classlike_checks = [];
491
492
    /**
493
     * Static methods to be called after classlikes have been scanned
494
     *
495
     * @var class-string<Hook\AfterClassLikeVisitInterface>[]
496
     */
497
    public $after_visit_classlikes = [];
498
499
    /**
500
     * Static methods to be called after codebase has been populated
501
     *
502
     * @var class-string<Hook\AfterCodebasePopulatedInterface>[]
503
     */
504
    public $after_codebase_populated = [];
505
506
    /**
507
     * Static methods to be called after codebase has been populated
508
     *
509
     * @var class-string<Hook\AfterAnalysisInterface>[]
510
     */
511
    public $after_analysis = [];
512
513
    /**
514
     * Static methods to be called after codebase has been populated
515
     * @var class-string<Hook\BeforeAnalyzeFileInterface>[]
516
     */
517
    public $before_analyze_file = [];
518
519
    /**
520
     * Static methods to be called after functionlike checks have completed
521
     *
522
     * @var class-string<Hook\AfterFunctionLikeAnalysisInterface>[]
523
     */
524
    public $after_functionlike_checks = [];
525
526
    /** @var array<string, mixed> */
527
    private $predefined_constants;
528
529
    /** @var array<callable-string, bool> */
530
    private $predefined_functions = [];
531
532
    /** @var ClassLoader|null */
533
    private $composer_class_loader;
534
535
    /**
536
     * Custom functions that always exit
537
     *
538
     * @var array<string, bool>
539
     */
540
    public $exit_functions = [];
541
542
    /**
543
     * @var string
544
     */
545
    public $hash = '';
546
547
    /** @var string|null */
548
    public $error_baseline = null;
549
550
    /**
551
     * @var bool
552
     */
553
    public $include_php_versions_in_error_baseline = false;
554
555
    /** @var string */
556
    public $shepherd_host = 'shepherd.dev';
557
558
    /**
559
     * @var array<string, string>
560
     */
561
    public $globals = [];
562
563
    /**
564
     * @var bool
565
     */
566
    public $parse_sql = false;
567
568
    /**
569
     * @var int
570
     */
571
    public $max_string_length = 1000;
572
573
    /** @var ?IncludeCollector */
574
    private $include_collector;
575
576
    /**
577
     * @var TaintAnalysisFileFilter|null
578
     */
579
    protected $taint_analysis_ignored_files;
580
581
    /**
582
     * @var bool whether to emit a backtrace of emitted issues to stderr
583
     */
584
    public $debug_emitted_issues = false;
585
586
    protected function __construct()
587
    {
588
        self::$instance = $this;
589
    }
590
591
    /**
592
     * Gets a Config object from an XML file.
593
     *
594
     * Searches up a folder hierarchy for the most immediate config.
595
     *
596
     * @param  string $path
597
     * @param  string $current_dir
598
     * @param  string $output_format
599
     *
600
     * @return Config
601
     * @throws ConfigException if a config path is not found
602
     *
603
     */
604
    public static function getConfigForPath($path, $current_dir, $output_format)
605
    {
606
        $config_path = self::locateConfigFile($path);
607
608
        if (!$config_path) {
609
            if ($output_format === \Psalm\Report::TYPE_CONSOLE) {
610
                echo 'Could not locate a config XML file in path ' . $path
611
                    . '. Have you run \'psalm --init\' ?' . PHP_EOL;
612
                exit(1);
613
            }
614
            throw new ConfigException('Config not found for path ' . $path);
615
        }
616
617
        return self::loadFromXMLFile($config_path, $current_dir);
618
    }
619
620
    /**
621
     * Searches up a folder hierarchy for the most immediate config.
622
     *
623
     * @throws ConfigException
624
     *
625
     * @return ?string
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?string" at position 0. (view supported doc-types)

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

Loading history...
626
     */
627
    public static function locateConfigFile(string $path)
628
    {
629
        $dir_path = realpath($path);
630
631
        if ($dir_path === false) {
632
            throw new ConfigException('Config not found for path ' . $path);
633
        }
634
635
        if (!is_dir($dir_path)) {
636
            $dir_path = dirname($dir_path);
637
        }
638
639
        do {
640
            $maybe_path = $dir_path . DIRECTORY_SEPARATOR . Config::DEFAULT_FILE_NAME;
641
642
            if (file_exists($maybe_path) || file_exists($maybe_path .= '.dist')) {
643
                return $maybe_path;
644
            }
645
646
            $dir_path = dirname($dir_path);
647
        } while (dirname($dir_path) !== $dir_path);
648
649
        return null;
650
    }
651
652
    /**
653
     * Creates a new config object from the file
654
     *
655
     * @param  string           $file_path
656
     * @param  string           $current_dir
657
     *
658
     * @return self
659
     */
660
    public static function loadFromXMLFile($file_path, $current_dir)
661
    {
662
        $file_contents = file_get_contents($file_path);
663
664
        $base_dir = dirname($file_path) . DIRECTORY_SEPARATOR;
665
666
        if ($file_contents === false) {
667
            throw new \InvalidArgumentException('Cannot open ' . $file_path);
668
        }
669
670
        try {
671
            $config = self::loadFromXML($base_dir, $file_contents, $current_dir);
672
            $config->hash = sha1($file_contents . \PSALM_VERSION);
673
        } catch (ConfigException $e) {
674
            throw new ConfigException(
675
                'Problem parsing ' . $file_path . ":\n" . '  ' . $e->getMessage()
676
            );
677
        }
678
679
        return $config;
680
    }
681
682
    /**
683
     * Creates a new config object from an XML string
684
     *
685
     * @throws ConfigException
686
     *
687
     * @param  string           $base_dir
688
     * @param  string           $file_contents
689
     * @param  string|null      $current_dir Current working directory, if different to $base_dir
690
     *
691
     * @return self
692
     */
693
    public static function loadFromXML($base_dir, $file_contents, $current_dir = null)
694
    {
695
        if ($current_dir === null) {
696
            $current_dir = $base_dir;
697
        }
698
699
        self::validateXmlConfig($base_dir, $file_contents);
700
701
        return self::fromXmlAndPaths($base_dir, $file_contents, $current_dir);
702
    }
703
704
    private static function loadDomDocument(string $base_dir, string $file_contents): DOMDocument
705
    {
706
        $dom_document = new DOMDocument();
707
708
        // there's no obvious way to set xml:base for a document when loading it from string
709
        // so instead we're changing the current directory instead to be able to process XIncludes
710
        $oldpwd = getcwd();
711
        chdir($base_dir);
712
713
        $dom_document->loadXML($file_contents, LIBXML_NONET);
714
        $dom_document->xinclude(LIBXML_NONET);
715
716
        chdir($oldpwd);
717
        return $dom_document;
718
    }
719
720
    /**
721
     * @throws ConfigException
722
     */
723
    private static function validateXmlConfig(string $base_dir, string $file_contents): void
724
    {
725
        $schema_path = dirname(dirname(__DIR__)) . '/config.xsd';
726
727
        if (!file_exists($schema_path)) {
728
            throw new ConfigException('Cannot locate config schema');
729
        }
730
731
        $dom_document = self::loadDomDocument($base_dir, $file_contents);
732
733
        $psalm_nodes = $dom_document->getElementsByTagName('psalm');
734
735
        /** @var \DomElement|null */
736
        $psalm_node = $psalm_nodes->item(0);
737
738
        if (!$psalm_node) {
739
            throw new ConfigException(
740
                'Missing psalm node'
741
            );
742
        }
743
744
        if (!$psalm_node->hasAttribute('xmlns')) {
745
            $psalm_node->setAttribute('xmlns', 'https://getpsalm.org/schema/config');
746
747
            $old_dom_document = $dom_document;
748
            $dom_document = self::loadDomDocument($base_dir, $old_dom_document->saveXML());
749
        }
750
751
        // Enable user error handling
752
        libxml_use_internal_errors(true);
753
754
        if (!$dom_document->schemaValidate($schema_path)) {
755
            $errors = libxml_get_errors();
756
            foreach ($errors as $error) {
757
                if ($error->level === LIBXML_ERR_FATAL || $error->level === LIBXML_ERR_ERROR) {
758
                    throw new ConfigException(
759
                        'Error on line ' . $error->line . ":\n" . '    ' . $error->message
760
                    );
761
                }
762
            }
763
            libxml_clear_errors();
764
        }
765
    }
766
767
768
    /**
769
     * @psalm-suppress MixedMethodCall
770
     * @psalm-suppress MixedAssignment
771
     * @psalm-suppress MixedOperand
772
     * @psalm-suppress MixedArgument
773
     * @psalm-suppress MixedPropertyFetch
774
     *
775
     * @throws ConfigException
776
     */
777
    private static function fromXmlAndPaths(string $base_dir, string $file_contents, string $current_dir): self
778
    {
779
        $config = new static();
780
781
        $dom_document = self::loadDomDocument($base_dir, $file_contents);
782
783
        $config_xml = simplexml_import_dom($dom_document);
784
785
        $booleanAttributes = [
786
            'useDocblockTypes' => 'use_docblock_types',
787
            'useDocblockPropertyTypes' => 'use_docblock_property_types',
788
            'throwExceptionOnError' => 'throw_exception',
789
            'hideExternalErrors' => 'hide_external_errors',
790
            'resolveFromConfigFile' => 'resolve_from_config_file',
791
            'allowFileIncludes' => 'allow_includes',
792
            'strictBinaryOperands' => 'strict_binary_operands',
793
            'requireVoidReturnType' => 'add_void_docblocks',
794
            'useAssertForType' => 'use_assert_for_type',
795
            'rememberPropertyAssignmentsAfterCall' => 'remember_property_assignments_after_call',
796
            'allowPhpStormGenerics' => 'allow_phpstorm_generics',
797
            'allowStringToStandInForClass' => 'allow_string_standin_for_class',
798
            'usePhpDocMethodsWithoutMagicCall' => 'use_phpdoc_method_without_magic_or_parent',
799
            'usePhpDocPropertiesWithoutMagicCall' => 'use_phpdoc_property_without_magic_or_parent',
800
            'memoizeMethodCallResults' => 'memoize_method_calls',
801
            'hoistConstants' => 'hoist_constants',
802
            'addParamDefaultToDocblockType' => 'add_param_default_to_docblock_type',
803
            'checkForThrowsDocblock' => 'check_for_throws_docblock',
804
            'checkForThrowsInGlobalScope' => 'check_for_throws_in_global_scope',
805
            'forbidEcho' => 'forbid_echo',
806
            'ignoreInternalFunctionFalseReturn' => 'ignore_internal_falsable_issues',
807
            'ignoreInternalFunctionNullReturn' => 'ignore_internal_nullable_issues',
808
            'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline',
809
            'loadXdebugStub' => 'load_xdebug_stub',
810
            'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
811
            'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist',
812
            'reportMixedIssues' => 'show_mixed_issues',
813
            'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes',
814
            'sealAllMethods' => 'seal_all_methods',
815
            'runTaintAnalysis' => 'run_taint_analysis',
816
        ];
817
818
        foreach ($booleanAttributes as $xmlName => $internalName) {
819
            if (isset($config_xml[$xmlName])) {
820
                $attribute_text = (string) $config_xml[$xmlName];
821
                $config->setBooleanAttribute(
822
                    $internalName,
823
                    $attribute_text === 'true' || $attribute_text === '1'
824
                );
825
            }
826
        }
827
828
        if ($config->resolve_from_config_file) {
829
            $config->base_dir = $base_dir;
830
        } else {
831
            $config->base_dir = $current_dir;
832
            $base_dir = $current_dir;
833
        }
834
835
        if (isset($config_xml['phpVersion'])) {
836
            $config->configured_php_version = (string) $config_xml['phpVersion'];
837
        }
838
839
        if (isset($config_xml['autoloader'])) {
840
            $autoloader_path = $config->base_dir . DIRECTORY_SEPARATOR . $config_xml['autoloader'];
841
842
            if (!file_exists($autoloader_path)) {
843
                throw new ConfigException('Cannot locate autoloader');
844
            }
845
846
            $config->autoloader = realpath($autoloader_path);
847
        }
848
849
        if (isset($config_xml['cacheDirectory'])) {
850
            $config->cache_directory = (string)$config_xml['cacheDirectory'];
851
        } else {
852
            $config->cache_directory = sys_get_temp_dir() . '/psalm';
853
        }
854
855
        $config->global_cache_directory = $config->cache_directory;
856
857
        $config->cache_directory .= DIRECTORY_SEPARATOR . sha1($base_dir);
858
859
        if (is_dir($config->cache_directory) === false && @mkdir($config->cache_directory, 0777, true) === false) {
860
            trigger_error('Could not create cache directory: ' . $config->cache_directory, E_USER_ERROR);
861
        }
862
863
        if (isset($config_xml['serializer'])) {
864
            $attribute_text = (string) $config_xml['serializer'];
865
            $config->use_igbinary = $attribute_text === 'igbinary';
866
        } elseif ($igbinary_version = phpversion('igbinary')) {
867
            $config->use_igbinary = version_compare($igbinary_version, '2.0.5') >= 0;
868
        }
869
870
871
        if (isset($config_xml['findUnusedCode'])) {
872
            $attribute_text = (string) $config_xml['findUnusedCode'];
873
            $config->find_unused_code = $attribute_text === 'true' || $attribute_text === '1';
874
            $config->find_unused_variables = $config->find_unused_code;
875
        }
876
877
        if (isset($config_xml['findUnusedVariablesAndParams'])) {
878
            $attribute_text = (string) $config_xml['findUnusedVariablesAndParams'];
879
            $config->find_unused_variables = $attribute_text === 'true' || $attribute_text === '1';
880
        }
881
882
        if (isset($config_xml['errorLevel'])) {
883
            $attribute_text = (int) $config_xml['errorLevel'];
884
885
            if (!in_array($attribute_text, [1, 2, 3, 4, 5, 6, 7, 8], true)) {
886
                throw new Exception\ConfigException(
887
                    'Invalid error level ' . $config_xml['errorLevel']
888
                );
889
            }
890
891
            $config->level = $attribute_text;
892
        } elseif (isset($config_xml['totallyTyped'])) {
893
            $totally_typed = (string) $config_xml['totallyTyped'];
894
895
            if ($totally_typed === 'true' || $totally_typed === '1') {
896
                $config->level = 1;
897
            } else {
898
                $config->level = 2;
899
900
                if ($config->show_mixed_issues === null) {
901
                    $config->show_mixed_issues = false;
902
                }
903
            }
904
        } else {
905
            $config->level = 2;
906
        }
907
908
        if (isset($config_xml['errorBaseline'])) {
909
            $attribute_text = (string) $config_xml['errorBaseline'];
910
            $config->error_baseline = $attribute_text;
911
        }
912
913
        if (isset($config_xml['maxStringLength'])) {
914
            $attribute_text = intval($config_xml['maxStringLength']);
915
            $config->max_string_length = $attribute_text;
916
        }
917
918
        if (isset($config_xml['parseSql'])) {
919
            $attribute_text = (string) $config_xml['parseSql'];
920
            $config->parse_sql = $attribute_text === 'true' || $attribute_text === '1';
921
        }
922
923
        if (isset($config_xml['inferPropertyTypesFromConstructor'])) {
924
            $attribute_text = (string) $config_xml['inferPropertyTypesFromConstructor'];
925
            $config->infer_property_types_from_constructor = $attribute_text === 'true' || $attribute_text === '1';
926
        }
927
928
        if (isset($config_xml->projectFiles)) {
929
            $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true);
930
        }
931
932
        if (isset($config_xml->extraFiles)) {
933
            $config->extra_files = ProjectFileFilter::loadFromXMLElement($config_xml->extraFiles, $base_dir, true);
934
        }
935
936
        if (isset($config_xml->taintAnalysis->ignoreFiles)) {
937
            $config->taint_analysis_ignored_files = TaintAnalysisFileFilter::loadFromXMLElement(
938
                $config_xml->taintAnalysis->ignoreFiles,
939
                $base_dir,
940
                false
941
            );
942
        }
943
944
        if (isset($config_xml->fileExtensions)) {
945
            $config->file_extensions = [];
946
947
            $config->loadFileExtensions($config_xml->fileExtensions->extension);
948
        }
949
950
        if (isset($config_xml->mockClasses) && isset($config_xml->mockClasses->class)) {
951
            /** @var \SimpleXMLElement $mock_class */
952
            foreach ($config_xml->mockClasses->class as $mock_class) {
953
                $config->mock_classes[] = strtolower((string)$mock_class['name']);
954
            }
955
        }
956
957
        if (isset($config_xml->ignoreExceptions)) {
958
            if (isset($config_xml->ignoreExceptions->class)) {
959
                /** @var \SimpleXMLElement $exception_class */
960
                foreach ($config_xml->ignoreExceptions->class as $exception_class) {
961
                    $exception_name = (string) $exception_class['name'];
962
                    $global_attribute_text = (string) $exception_class['onlyGlobalScope'];
963
                    if ($global_attribute_text !== 'true' && $global_attribute_text !== '1') {
964
                        $config->ignored_exceptions[$exception_name] = true;
965
                    }
966
                    $config->ignored_exceptions_in_global_scope[$exception_name] = true;
967
                }
968
            }
969
            if (isset($config_xml->ignoreExceptions->classAndDescendants)) {
970
                /** @var \SimpleXMLElement $exception_class */
971
                foreach ($config_xml->ignoreExceptions->classAndDescendants as $exception_class) {
972
                    $exception_name = (string) $exception_class['name'];
973
                    $global_attribute_text = (string) $exception_class['onlyGlobalScope'];
974
                    if ($global_attribute_text !== 'true' && $global_attribute_text !== '1') {
975
                        $config->ignored_exceptions_and_descendants[$exception_name] = true;
976
                    }
977
                    $config->ignored_exceptions_and_descendants_in_global_scope[$exception_name] = true;
978
                }
979
            }
980
        }
981
982
        if (isset($config_xml->forbiddenFunctions) && isset($config_xml->forbiddenFunctions->function)) {
983
            /** @var \SimpleXMLElement $forbidden_function */
984
            foreach ($config_xml->forbiddenFunctions->function as $forbidden_function) {
985
                $config->forbidden_functions[strtolower((string) $forbidden_function['name'])] = true;
986
            }
987
        }
988
989
        if (isset($config_xml->exitFunctions) && isset($config_xml->exitFunctions->function)) {
990
            /** @var \SimpleXMLElement $exit_function */
991
            foreach ($config_xml->exitFunctions->function as $exit_function) {
992
                $config->exit_functions[strtolower((string) $exit_function['name'])] = true;
993
            }
994
        }
995
996
        if (isset($config_xml->stubs) && isset($config_xml->stubs->file)) {
997
            /** @var \SimpleXMLElement $stub_file */
998
            foreach ($config_xml->stubs->file as $stub_file) {
999
                $stub_file_name = (string)$stub_file['name'];
1000
                if (!Path::isAbsolute($stub_file_name)) {
1001
                    $stub_file_name = $config->base_dir . DIRECTORY_SEPARATOR . $stub_file_name;
1002
                }
1003
                $file_path = realpath($stub_file_name);
1004
1005
                if (!$file_path) {
1006
                    throw new Exception\ConfigException(
1007
                        'Cannot resolve stubfile path ' . $config->base_dir . DIRECTORY_SEPARATOR . $stub_file['name']
1008
                    );
1009
                }
1010
1011
                $config->addStubFile($file_path);
1012
            }
1013
        }
1014
1015
        // this plugin loading system borrows heavily from etsy/phan
1016
        if (isset($config_xml->plugins)) {
1017
            if (isset($config_xml->plugins->plugin)) {
1018
                /** @var \SimpleXMLElement $plugin */
1019
                foreach ($config_xml->plugins->plugin as $plugin) {
1020
                    $plugin_file_name = (string) $plugin['filename'];
1021
1022
                    $path = Path::isAbsolute($plugin_file_name)
1023
                        ? $plugin_file_name
1024
                        : $config->base_dir . $plugin_file_name;
1025
1026
                    $config->addPluginPath($path);
1027
                }
1028
            }
1029
            if (isset($config_xml->plugins->pluginClass)) {
1030
                /** @var \SimpleXMLElement $plugin */
1031
                foreach ($config_xml->plugins->pluginClass as $plugin) {
1032
                    $plugin_class_name = $plugin['class'];
1033
                    // any child elements are used as plugin configuration
1034
                    $plugin_config = null;
1035
                    if ($plugin->count()) {
1036
                        $plugin_config = $plugin->children();
1037
                    }
1038
1039
                    $config->addPluginClass((string) $plugin_class_name, $plugin_config);
1040
                }
1041
            }
1042
        }
1043
1044
        if (isset($config_xml->issueHandlers)) {
1045
            /** @var \SimpleXMLElement $issue_handler */
1046
            foreach ($config_xml->issueHandlers->children() as $key => $issue_handler) {
1047
                if ($key === 'PluginIssue') {
1048
                    $custom_class_name = (string) $issue_handler['name'];
1049
                    /** @var string $key */
1050
                    $config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement(
1051
                        $issue_handler,
1052
                        $base_dir
1053
                    );
1054
                } else {
1055
                    /** @var string $key */
1056
                    $config->issue_handlers[$key] = IssueHandler::loadFromXMLElement(
1057
                        $issue_handler,
1058
                        $base_dir
1059
                    );
1060
                }
1061
            }
1062
        }
1063
1064
        if (isset($config_xml->globals) && isset($config_xml->globals->var)) {
1065
            /** @var \SimpleXMLElement $var */
1066
            foreach ($config_xml->globals->var as $var) {
1067
                $config->globals['$' . (string) $var['name']] = (string) $var['type'];
1068
            }
1069
        }
1070
1071
        return $config;
1072
    }
1073
1074
    /**
1075
     * @return $this
1076
     */
1077
    public static function getInstance()
1078
    {
1079
        if (self::$instance) {
1080
            return self::$instance;
1081
        }
1082
1083
        throw new \UnexpectedValueException('No config initialized');
1084
    }
1085
1086
    /**
1087
     * @return void
1088
     */
1089
    public function setComposerClassLoader(?ClassLoader $loader = null)
1090
    {
1091
        $this->composer_class_loader = $loader;
1092
    }
1093
1094
    /**
1095
     * @param string $issue_key
1096
     * @param string $error_level
1097
     *
1098
     * @return void
1099
     */
1100
    public function setCustomErrorLevel($issue_key, $error_level)
1101
    {
1102
        $this->issue_handlers[$issue_key] = new IssueHandler();
1103
        $this->issue_handlers[$issue_key]->setErrorLevel($error_level);
1104
    }
1105
1106
    /**
1107
     * @param  array<SimpleXMLElement> $extensions
1108
     *
1109
     * @throws ConfigException if a Config file could not be found
1110
     *
1111
     * @return void
1112
     */
1113
    private function loadFileExtensions($extensions)
1114
    {
1115
        foreach ($extensions as $extension) {
1116
            $extension_name = preg_replace('/^\.?/', '', (string)$extension['name']);
1117
            $this->file_extensions[] = $extension_name;
1118
1119
            if (isset($extension['scanner'])) {
1120
                $path = $this->base_dir . (string)$extension['scanner'];
1121
1122
                if (!file_exists($path)) {
1123
                    throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
1124
                }
1125
1126
                $this->filetype_scanner_paths[$extension_name] = $path;
1127
            }
1128
1129
            if (isset($extension['checker'])) {
1130
                $path = $this->base_dir . (string)$extension['checker'];
1131
1132
                if (!file_exists($path)) {
1133
                    throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
1134
                }
1135
1136
                $this->filetype_analyzer_paths[$extension_name] = $path;
1137
            }
1138
        }
1139
    }
1140
1141
    /**
1142
     * @param string $path
1143
     *
1144
     * @return void
1145
     */
1146
    public function addPluginPath($path)
1147
    {
1148
        if (!file_exists($path)) {
1149
            throw new \InvalidArgumentException('Cannot find plugin file ' . $path);
1150
        }
1151
1152
        $this->plugin_paths[] = $path;
1153
    }
1154
1155
    /** @return void */
1156
    public function addPluginClass(string $class_name, SimpleXMLElement $plugin_config = null)
1157
    {
1158
        $this->plugin_classes[] = ['class' => $class_name, 'config' => $plugin_config];
1159
    }
1160
1161
    /** @return array<array{class:string, config:?SimpleXmlElement}> */
0 ignored issues
show
Documentation introduced by
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...
1162
    public function getPluginClasses(): array
1163
    {
1164
        return $this->plugin_classes;
1165
    }
1166
1167
    /**
1168
     * Initialises all the plugins (done once the config is fully loaded)
1169
     *
1170
     * @return void
1171
     * @psalm-suppress MixedAssignment
1172
     * @psalm-suppress MixedTypeCoercion
1173
     */
1174
    public function initializePlugins(ProjectAnalyzer $project_analyzer)
1175
    {
1176
        $codebase = $project_analyzer->getCodebase();
1177
1178
        $project_analyzer->progress->debug('Initializing plugins...' . PHP_EOL);
1179
1180
        $socket = new PluginRegistrationSocket($this, $codebase);
1181
        // initialize plugin classes earlier to let them hook into subsequent load process
1182
        foreach ($this->plugin_classes as $plugin_class_entry) {
1183
            $plugin_class_name = $plugin_class_entry['class'];
1184
            $plugin_config = $plugin_class_entry['config'];
1185
1186
            try {
1187
                // Below will attempt to load plugins from the project directory first.
1188
                // Failing that, it will use registered autoload chain, which will load
1189
                // plugins from Psalm directory or phar file. If that fails as well, it
1190
                // will fall back to project autoloader. It may seem that the last step
1191
                // will always fail, but it's only true if project uses Composer autoloader
1192
                if ($this->composer_class_loader
1193
                    && ($plugin_class_path = $this->composer_class_loader->findFile($plugin_class_name))
1194
                ) {
1195
                    $project_analyzer->progress->debug(
1196
                        'Loading plugin ' . $plugin_class_name . ' via require'. PHP_EOL
1197
                    );
1198
1199
                    /** @psalm-suppress UnresolvableInclude */
1200
                    require_once($plugin_class_path);
1201
                } else {
1202
                    if (!class_exists($plugin_class_name, true)) {
1203
                        throw new \UnexpectedValueException($plugin_class_name . ' is not a known class');
1204
                    }
1205
                }
1206
1207
                /**
1208
                 * @psalm-suppress InvalidStringClass
1209
                 *
1210
                 * @var Plugin\PluginEntryPointInterface
1211
                 */
1212
                $plugin_object = new $plugin_class_name;
1213
                $plugin_object($socket, $plugin_config);
1214
            } catch (\Throwable $e) {
1215
                throw new ConfigException('Failed to load plugin ' . $plugin_class_name, 0, $e);
1216
            }
1217
1218
            $project_analyzer->progress->debug('Loaded plugin ' . $plugin_class_name . ' successfully'. PHP_EOL);
1219
        }
1220
1221
        foreach ($this->filetype_scanner_paths as $extension => $path) {
1222
            $fq_class_name = $this->getPluginClassForPath(
1223
                $codebase,
1224
                $path,
1225
                FileScanner::class
1226
            );
1227
1228
            /** @psalm-suppress UnresolvableInclude */
1229
            require_once($path);
1230
1231
            $this->filetype_scanners[$extension] = $fq_class_name;
1232
        }
1233
1234
        foreach ($this->filetype_analyzer_paths as $extension => $path) {
1235
            $fq_class_name = $this->getPluginClassForPath(
1236
                $codebase,
1237
                $path,
1238
                FileAnalyzer::class
1239
            );
1240
1241
            /** @psalm-suppress UnresolvableInclude */
1242
            require_once($path);
1243
1244
            $this->filetype_analyzers[$extension] = $fq_class_name;
1245
        }
1246
1247
        foreach ($this->plugin_paths as $path) {
1248
            try {
1249
                $plugin_object = new FileBasedPluginAdapter($path, $this, $codebase);
1250
                $plugin_object($socket);
1251
            } catch (\Throwable $e) {
1252
                throw new ConfigException('Failed to load plugin ' . $path, 0, $e);
1253
            }
1254
        }
1255
    }
1256
1257
    /**
1258
     * @template T
1259
     *
1260
     * @param  string $path
1261
     * @param  T::class $must_extend
0 ignored issues
show
Documentation introduced by
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...
1262
     *
1263
     * @return class-string<T>
0 ignored issues
show
Documentation introduced by
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...
1264
     */
1265
    private function getPluginClassForPath(Codebase $codebase, $path, $must_extend)
1266
    {
1267
        $file_storage = $codebase->createFileStorageForPath($path);
1268
        $file_to_scan = new FileScanner($path, $this->shortenFileName($path), true);
1269
        $file_to_scan->scan(
1270
            $codebase,
1271
            $file_storage
1272
        );
1273
1274
        $declared_classes = ClassLikeAnalyzer::getClassesForFile($codebase, $path);
1275
1276
        if (!count($declared_classes)) {
1277
            throw new \InvalidArgumentException(
1278
                'Plugins must have at least one class in the file - ' . $path . ' has ' .
1279
                    count($declared_classes)
1280
            );
1281
        }
1282
1283
        $fq_class_name = reset($declared_classes);
1284
1285
        if (!$codebase->classlikes->classExtends(
1286
            $fq_class_name,
1287
            $must_extend
1288
        )
1289
        ) {
1290
            throw new \InvalidArgumentException(
1291
                'This plugin must extend ' . $must_extend . ' - ' . $path . ' does not'
1292
            );
1293
        }
1294
1295
        /**
1296
         * @var class-string<T>
1297
         */
1298
        return $fq_class_name;
1299
    }
1300
1301
    /**
1302
     * @param  string $file_name
1303
     *
1304
     * @return string
1305
     */
1306
    public function shortenFileName($file_name)
1307
    {
1308
        return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $file_name);
1309
    }
1310
1311
    /**
1312
     * @param   string $issue_type
1313
     * @param   string $file_path
1314
     *
1315
     * @return  bool
1316
     */
1317
    public function reportIssueInFile($issue_type, $file_path)
1318
    {
1319
        if (($this->show_mixed_issues === false || $this->level > 2)
1320
            && in_array($issue_type, self::MIXED_ISSUES, true)
1321
        ) {
1322
            return false;
1323
        }
1324
1325
        if ($this->mustBeIgnored($file_path)) {
1326
            return false;
1327
        }
1328
1329
        $dependent_files = [strtolower($file_path) => $file_path];
1330
1331
        $project_analyzer = ProjectAnalyzer::getInstance();
1332
1333
        $codebase = $project_analyzer->getCodebase();
1334
1335
        if (!$this->hide_external_errors) {
1336
            try {
1337
                $file_storage = $codebase->file_storage_provider->get($file_path);
1338
                $dependent_files += $file_storage->required_by_file_paths;
1339
            } catch (\InvalidArgumentException $e) {
1340
                // do nothing
1341
            }
1342
        }
1343
1344
        $any_file_path_matched = false;
1345
1346
        foreach ($dependent_files as $dependent_file_path) {
1347
            if (((!$project_analyzer->full_run && $codebase->analyzer->canReportIssues($dependent_file_path))
1348
                    || $project_analyzer->canReportIssues($dependent_file_path))
1349
                && ($file_path === $dependent_file_path || !$this->mustBeIgnored($dependent_file_path))
1350
            ) {
1351
                $any_file_path_matched = true;
1352
                break;
1353
            }
1354
        }
1355
1356
        if (!$any_file_path_matched) {
1357
            return false;
1358
        }
1359
1360
        if ($this->getReportingLevelForFile($issue_type, $file_path) === self::REPORT_SUPPRESS) {
1361
            return false;
1362
        }
1363
1364
        return true;
1365
    }
1366
1367
    /**
1368
     * @param   string $file_path
1369
     *
1370
     * @return  bool
1371
     */
1372
    public function isInProjectDirs($file_path)
1373
    {
1374
        return $this->project_files && $this->project_files->allows($file_path);
1375
    }
1376
1377
    /**
1378
     * @param   string $file_path
1379
     *
1380
     * @return  bool
1381
     */
1382
    public function isInExtraDirs($file_path)
1383
    {
1384
        return $this->extra_files && $this->extra_files->allows($file_path);
1385
    }
1386
1387
    /**
1388
     * @param   string $file_path
1389
     *
1390
     * @return  bool
1391
     */
1392
    public function mustBeIgnored($file_path)
1393
    {
1394
        return $this->project_files && $this->project_files->forbids($file_path);
1395
    }
1396
1397
    public function trackTaintsInPath(string $file_path) : bool
1398
    {
1399
        return !$this->taint_analysis_ignored_files
1400
            || $this->taint_analysis_ignored_files->allows($file_path);
1401
    }
1402
1403
    public function getReportingLevelForIssue(CodeIssue $e) : string
1404
    {
1405
        $fqcn_parts = explode('\\', get_class($e));
1406
        $issue_type = array_pop($fqcn_parts);
1407
1408
        $reporting_level = null;
1409
1410
        if ($e instanceof ClassIssue) {
1411
            $reporting_level = $this->getReportingLevelForClass($issue_type, $e->fq_classlike_name);
1412
        } elseif ($e instanceof MethodIssue) {
1413
            $reporting_level = $this->getReportingLevelForMethod($issue_type, $e->method_id);
1414
        } elseif ($e instanceof FunctionIssue) {
1415
            $reporting_level = $this->getReportingLevelForFunction($issue_type, $e->function_id);
1416
        } elseif ($e instanceof PropertyIssue) {
1417
            $reporting_level = $this->getReportingLevelForProperty($issue_type, $e->property_id);
1418
        } elseif ($e instanceof ArgumentIssue && $e->function_id) {
1419
            $reporting_level = $this->getReportingLevelForArgument($issue_type, $e->function_id);
1420
        } elseif ($e instanceof VariableIssue) {
1421
            $reporting_level = $this->getReportingLevelForVariable($issue_type, $e->var_name);
1422
        }
1423
1424
        if ($reporting_level === null) {
1425
            $reporting_level = $this->getReportingLevelForFile($issue_type, $e->getFilePath());
1426
        }
1427
1428
        $parent_issue_type = self::getParentIssueType($issue_type);
1429
1430
        if ($parent_issue_type && $reporting_level === Config::REPORT_ERROR) {
1431
            $parent_reporting_level = $this->getReportingLevelForFile($parent_issue_type, $e->getFilePath());
1432
1433
            if ($parent_reporting_level !== $reporting_level) {
1434
                return $parent_reporting_level;
1435
            }
1436
        }
1437
1438
        return $reporting_level;
1439
    }
1440
1441
    /**
1442
     * @param  string $issue_type
1443
     *
1444
     * @return string|null
1445
     */
1446
    public static function getParentIssueType($issue_type)
1447
    {
1448
        if ($issue_type === 'PossiblyUndefinedIntArrayOffset'
1449
            || $issue_type === 'PossiblyUndefinedStringArrayOffset'
1450
        ) {
1451
            return 'PossiblyUndefinedArrayOffset';
1452
        }
1453
1454
        if ($issue_type === 'PossiblyNullReference') {
1455
            return 'NullReference';
1456
        }
1457
1458
        if ($issue_type === 'PossiblyFalseReference') {
1459
            return null;
1460
        }
1461
1462
        if ($issue_type === 'PossiblyUndefinedArrayOffset') {
1463
            return null;
1464
        }
1465
1466
        if (strpos($issue_type, 'Possibly') === 0) {
1467
            $stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type);
1468
1469
            if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) {
1470
                $stripped_issue_type = 'Invalid' . $stripped_issue_type;
1471
            }
1472
1473
            return $stripped_issue_type;
1474
        }
1475
1476
        if (preg_match('/^(False|Null)[A-Z]/', $issue_type) && !strpos($issue_type, 'Reference')) {
1477
            return preg_replace('/^(False|Null)/', 'Invalid', $issue_type);
1478
        }
1479
1480
        if ($issue_type === 'UndefinedInterfaceMethod') {
1481
            return 'UndefinedMethod';
1482
        }
1483
1484
        if ($issue_type === 'UndefinedMagicPropertyFetch') {
1485
            return 'UndefinedPropertyFetch';
1486
        }
1487
1488
        if ($issue_type === 'UndefinedMagicPropertyAssignment') {
1489
            return 'UndefinedPropertyAssignment';
1490
        }
1491
1492
        if ($issue_type === 'UndefinedMagicMethod') {
1493
            return 'UndefinedMethod';
1494
        }
1495
1496
        if ($issue_type === 'PossibleRawObjectIteration') {
1497
            return 'RawObjectIteration';
1498
        }
1499
1500
        if ($issue_type === 'UninitializedProperty') {
1501
            return 'PropertyNotSetInConstructor';
1502
        }
1503
1504
        if ($issue_type === 'InvalidDocblockParamName') {
1505
            return 'InvalidDocblock';
1506
        }
1507
1508
        if ($issue_type === 'UnusedClosureParam') {
1509
            return 'UnusedParam';
1510
        }
1511
1512
        if ($issue_type === 'StringIncrement') {
1513
            return 'InvalidOperand';
1514
        }
1515
1516
        if ($issue_type === 'TraitMethodSignatureMismatch') {
1517
            return 'MethodSignatureMismatch';
1518
        }
1519
1520
        if ($issue_type === 'ImplementedParamTypeMismatch') {
1521
            return 'MoreSpecificImplementedParamType';
1522
        }
1523
1524
        if ($issue_type === 'UndefinedDocblockClass') {
1525
            return 'UndefinedClass';
1526
        }
1527
1528
        if ($issue_type === 'MixedArgumentTypeCoercion'
1529
            || $issue_type === 'MixedPropertyTypeCoercion'
1530
            || $issue_type === 'MixedReturnTypeCoercion'
1531
            || $issue_type === 'MixedArrayTypeCoercion'
1532
        ) {
1533
            return 'MixedTypeCoercion';
1534
        }
1535
1536
        if ($issue_type === 'ArgumentTypeCoercion'
1537
            || $issue_type === 'PropertyTypeCoercion'
1538
            || $issue_type === 'ReturnTypeCoercion'
1539
        ) {
1540
            return 'TypeCoercion';
1541
        }
1542
1543
        return null;
1544
    }
1545
1546
    /**
1547
     * @param   string $issue_type
1548
     * @param   string $file_path
1549
     *
1550
     * @return  string
1551
     */
1552
    public function getReportingLevelForFile($issue_type, $file_path)
1553
    {
1554
        if (isset($this->issue_handlers[$issue_type])) {
1555
            return $this->issue_handlers[$issue_type]->getReportingLevelForFile($file_path);
1556
        }
1557
1558
        // this string is replaced by scoper for Phars, so be careful
1559
        $issue_class = 'Psalm\\Issue\\' . $issue_type;
1560
1561
        if (!class_exists($issue_class) || !is_a($issue_class, \Psalm\Issue\CodeIssue::class, true)) {
1562
            return self::REPORT_ERROR;
1563
        }
1564
1565
        /** @var int */
1566
        $issue_level = $issue_class::ERROR_LEVEL;
1567
1568
        if ($issue_level > 0 && $issue_level < $this->level) {
1569
            return self::REPORT_INFO;
1570
        }
1571
1572
        return self::REPORT_ERROR;
1573
    }
1574
1575
    /**
1576
     * @param   string $issue_type
1577
     * @param   string $fq_classlike_name
1578
     *
1579
     * @return  string|null
1580
     */
1581
    public function getReportingLevelForClass($issue_type, $fq_classlike_name)
1582
    {
1583
        if (isset($this->issue_handlers[$issue_type])) {
1584
            return $this->issue_handlers[$issue_type]->getReportingLevelForClass($fq_classlike_name);
1585
        }
1586
    }
1587
1588
    /**
1589
     * @param   string $issue_type
1590
     * @param   string $method_id
1591
     *
1592
     * @return  string|null
1593
     */
1594
    public function getReportingLevelForMethod($issue_type, $method_id)
1595
    {
1596
        if (isset($this->issue_handlers[$issue_type])) {
1597
            return $this->issue_handlers[$issue_type]->getReportingLevelForMethod($method_id);
1598
        }
1599
    }
1600
1601
    /**
1602
     * @return  string|null
1603
     */
1604
    public function getReportingLevelForFunction(string $issue_type, string $function_id)
1605
    {
1606
        if (isset($this->issue_handlers[$issue_type])) {
1607
            return $this->issue_handlers[$issue_type]->getReportingLevelForFunction($function_id);
1608
        }
1609
    }
1610
1611
    /**
1612
     * @return  string|null
1613
     */
1614
    public function getReportingLevelForArgument(string $issue_type, string $function_id)
1615
    {
1616
        if (isset($this->issue_handlers[$issue_type])) {
1617
            return $this->issue_handlers[$issue_type]->getReportingLevelForArgument($function_id);
1618
        }
1619
    }
1620
1621
    /**
1622
     * @param   string $issue_type
1623
     * @param   string $property_id
1624
     *
1625
     * @return  string|null
1626
     */
1627
    public function getReportingLevelForProperty($issue_type, $property_id)
1628
    {
1629
        if (isset($this->issue_handlers[$issue_type])) {
1630
            return $this->issue_handlers[$issue_type]->getReportingLevelForProperty($property_id);
1631
        }
1632
    }
1633
1634
    /**
1635
     * @param   string $issue_type
1636
     * @param   string $var_name
1637
     *
1638
     * @return  string|null
1639
     */
1640
    public function getReportingLevelForVariable(string $issue_type, string $var_name)
1641
    {
1642
        if (isset($this->issue_handlers[$issue_type])) {
1643
            return $this->issue_handlers[$issue_type]->getReportingLevelForVariable($var_name);
1644
        }
1645
    }
1646
1647
    /**
1648
     * @return array<string>
1649
     */
1650
    public function getProjectDirectories()
1651
    {
1652
        if (!$this->project_files) {
1653
            return [];
1654
        }
1655
1656
        return $this->project_files->getDirectories();
1657
    }
1658
1659
    /**
1660
     * @return array<string>
1661
     */
1662
    public function getProjectFiles()
1663
    {
1664
        if (!$this->project_files) {
1665
            return [];
1666
        }
1667
1668
        return $this->project_files->getFiles();
1669
    }
1670
1671
    /**
1672
     * @return array<string>
1673
     */
1674
    public function getExtraDirectories()
1675
    {
1676
        if (!$this->extra_files) {
1677
            return [];
1678
        }
1679
1680
        return $this->extra_files->getDirectories();
1681
    }
1682
1683
    /**
1684
     * @param   string $file_path
1685
     *
1686
     * @return  bool
1687
     */
1688
    public function reportTypeStatsForFile($file_path)
1689
    {
1690
        return $this->project_files
1691
            && $this->project_files->allows($file_path)
1692
            && $this->project_files->reportTypeStats($file_path);
1693
    }
1694
1695
    /**
1696
     * @param   string $file_path
1697
     *
1698
     * @return  bool
1699
     */
1700
    public function useStrictTypesForFile($file_path)
1701
    {
1702
        return $this->project_files && $this->project_files->useStrictTypes($file_path);
1703
    }
1704
1705
    /**
1706
     * @return array<string>
1707
     */
1708
    public function getFileExtensions()
1709
    {
1710
        return $this->file_extensions;
1711
    }
1712
1713
    /**
1714
     * @return array<string, class-string<FileScanner>>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

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

Loading history...
1715
     */
1716
    public function getFiletypeScanners()
1717
    {
1718
        return $this->filetype_scanners;
1719
    }
1720
1721
    /**
1722
     * @return array<string, class-string<FileAnalyzer>>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

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

Loading history...
1723
     */
1724
    public function getFiletypeAnalyzers()
1725
    {
1726
        return $this->filetype_analyzers;
1727
    }
1728
1729
    /**
1730
     * @return array<int, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

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

Loading history...
1731
     */
1732
    public function getMockClasses()
1733
    {
1734
        return $this->mock_classes;
1735
    }
1736
1737
    /**
1738
     * @return void
1739
     */
1740
    public function visitStubFiles(Codebase $codebase, Progress $progress = null)
1741
    {
1742
        if ($progress === null) {
1743
            $progress = new VoidProgress();
1744
        }
1745
1746
        $codebase->register_stub_files = true;
1747
1748
        // note: don't realpath $generic_stubs_path, or phar version will fail
1749
        $generic_stubs_path = __DIR__ . '/Internal/Stubs/CoreGenericFunctions.phpstub';
1750
1751
        if (!file_exists($generic_stubs_path)) {
1752
            throw new \UnexpectedValueException('Cannot locate core generic stubs');
1753
        }
1754
1755
        // note: don't realpath $generic_classes_path, or phar version will fail
1756
        $generic_classes_path = __DIR__ . '/Internal/Stubs/CoreGenericClasses.phpstub';
1757
1758
        if (!file_exists($generic_classes_path)) {
1759
            throw new \UnexpectedValueException('Cannot locate core generic classes');
1760
        }
1761
1762
        // note: don't realpath $generic_classes_path, or phar version will fail
1763
        $immutable_classes_path = __DIR__ . '/Internal/Stubs/CoreImmutableClasses.phpstub';
1764
1765
        if (!file_exists($immutable_classes_path)) {
1766
            throw new \UnexpectedValueException('Cannot locate core immutable classes');
1767
        }
1768
1769
        $core_generic_files = [$generic_stubs_path, $generic_classes_path, $immutable_classes_path];
1770
1771
        if (\extension_loaded('ds')) {
1772
            $ext_ds_path = __DIR__ . '/Internal/Stubs/ext-ds.php';
1773
1774
            if (!file_exists($ext_ds_path)) {
1775
                throw new \UnexpectedValueException('Cannot locate core generic classes');
1776
            }
1777
1778
            $core_generic_files[] = $ext_ds_path;
1779
        }
1780
1781
        $stub_files = array_merge($core_generic_files, $this->stub_files);
1782
1783
        $phpstorm_meta_path = $this->base_dir . DIRECTORY_SEPARATOR . '.phpstorm.meta.php';
1784
1785
        if (is_file($phpstorm_meta_path)) {
1786
            $stub_files[] = $phpstorm_meta_path;
1787
        } elseif (is_dir($phpstorm_meta_path)) {
1788
            $phpstorm_meta_path = realpath($phpstorm_meta_path);
1789
1790
            foreach (glob($phpstorm_meta_path . '/*.meta.php', GLOB_NOSORT) as $glob) {
1791
                if (is_file($glob) && realpath(dirname($glob)) === $phpstorm_meta_path) {
1792
                    $stub_files[] = $glob;
1793
                }
1794
            }
1795
        }
1796
1797
        if ($this->load_xdebug_stub) {
1798
            $xdebug_stub_path = __DIR__ . '/Internal/Stubs/Xdebug.php';
1799
1800
            if (!file_exists($xdebug_stub_path)) {
1801
                throw new \UnexpectedValueException('Cannot locate XDebug stub');
1802
            }
1803
1804
            $stub_files[] = $xdebug_stub_path;
1805
        }
1806
1807
        foreach ($stub_files as $file_path) {
1808
            $file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
1809
            $codebase->scanner->addFileToDeepScan($file_path);
1810
        }
1811
1812
        $progress->debug('Registering stub files' . "\n");
1813
1814
        $codebase->scanFiles();
1815
1816
        $progress->debug('Finished registering stub files' . "\n");
1817
1818
        $codebase->register_stub_files = false;
1819
    }
1820
1821
    /**
1822
     * @return string
1823
     */
1824
    public function getCacheDirectory()
1825
    {
1826
        return $this->cache_directory;
1827
    }
1828
1829
    /**
1830
     * @return ?string
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?string" at position 0. (view supported doc-types)

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

Loading history...
1831
     */
1832
    public function getGlobalCacheDirectory()
1833
    {
1834
        return $this->global_cache_directory;
1835
    }
1836
1837
    /**
1838
     * @return array<string, mixed>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

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

Loading history...
1839
     */
1840
    public function getPredefinedConstants()
1841
    {
1842
        return $this->predefined_constants;
1843
    }
1844
1845
    /**
1846
     * @return void
1847
     */
1848
    public function collectPredefinedConstants()
1849
    {
1850
        $this->predefined_constants = get_defined_constants();
1851
    }
1852
1853
    /**
1854
     * @return array<callable-string, bool>
0 ignored issues
show
Documentation introduced by
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...
1855
     */
1856
    public function getPredefinedFunctions()
1857
    {
1858
        return $this->predefined_functions;
1859
    }
1860
1861
    /**
1862
     * @return void
1863
     */
1864
    public function collectPredefinedFunctions()
1865
    {
1866
        $defined_functions = get_defined_functions();
1867
1868
        if (isset($defined_functions['user'])) {
1869
            foreach ($defined_functions['user'] as $function_name) {
1870
                $this->predefined_functions[$function_name] = true;
1871
            }
1872
        }
1873
1874
        if (isset($defined_functions['internal'])) {
1875
            foreach ($defined_functions['internal'] as $function_name) {
1876
                $this->predefined_functions[$function_name] = true;
1877
            }
1878
        }
1879
    }
1880
1881
    public function setIncludeCollector(IncludeCollector $include_collector): void
1882
    {
1883
        $this->include_collector = $include_collector;
1884
    }
1885
1886
    /**
1887
     * @return void
1888
     *
1889
     * @psalm-suppress MixedAssignment
1890
     * @psalm-suppress MixedArrayAccess
1891
     */
1892
    public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, Progress $progress = null)
1893
    {
1894
        if ($progress === null) {
1895
            $progress = new VoidProgress();
1896
        }
1897
1898
        if (!$this->include_collector) {
1899
            throw new LogicException("IncludeCollector should be set at this point");
1900
        }
1901
1902
        $this->collectPredefinedConstants();
1903
        $this->collectPredefinedFunctions();
1904
1905
        $vendor_autoload_files_path
1906
            = $this->base_dir . DIRECTORY_SEPARATOR . 'vendor'
1907
                . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_files.php';
1908
1909
        if (file_exists($vendor_autoload_files_path)) {
1910
            $this->include_collector->runAndCollect(
1911
                function () use ($vendor_autoload_files_path) {
1912
                    /**
1913
                     * @psalm-suppress UnresolvableInclude
1914
                     * @var string[]
1915
                     */
1916
                    return require $vendor_autoload_files_path;
1917
                }
1918
            );
1919
        }
1920
1921
        $codebase = $project_analyzer->getCodebase();
1922
1923
        if ($this->autoloader) {
1924
            // somee classes that we think are missing may not actually be missing
1925
            // as they might be autoloadable once we require the autoloader below
1926
            $codebase->classlikes->forgetMissingClassLikes();
1927
1928
            $this->include_collector->runAndCollect(
1929
                function () {
1930
                    // do this in a separate method so scope does not leak
1931
                    /** @psalm-suppress UnresolvableInclude */
1932
                    require $this->autoloader;
1933
                }
1934
            );
1935
        }
1936
1937
        $autoload_included_files = $this->include_collector->getFilteredIncludedFiles();
1938
1939
        if ($autoload_included_files) {
1940
            $codebase->register_autoload_files = true;
1941
1942
            $progress->debug('Registering autoloaded files' . "\n");
1943
            foreach ($autoload_included_files as $file_path) {
1944
                $file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
1945
                $progress->debug('   ' . $file_path . "\n");
1946
                $codebase->scanner->addFileToDeepScan($file_path);
1947
            }
1948
1949
            $codebase->scanner->scanFiles($codebase->classlikes);
1950
1951
            $progress->debug('Finished registering autoloaded files' . "\n");
1952
1953
            $codebase->register_autoload_files = false;
1954
        }
1955
    }
1956
1957
    /**
1958
     * @param  string $fq_classlike_name
1959
     *
1960
     * @return string|false
1961
     */
1962
    public function getComposerFilePathForClassLike($fq_classlike_name)
1963
    {
1964
        if (!$this->composer_class_loader) {
1965
            return false;
1966
        }
1967
1968
        return $this->composer_class_loader->findFile($fq_classlike_name);
1969
    }
1970
1971
    public function getPotentialComposerFilePathForClassLike(string $class) : ?string
1972
    {
1973
        if (!$this->composer_class_loader) {
1974
            return null;
1975
        }
1976
1977
        /** @var array<string, array<int, string>> */
1978
        $psr4_prefixes = $this->composer_class_loader->getPrefixesPsr4();
1979
1980
        // PSR-4 lookup
1981
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';
1982
1983
        $candidate_path = null;
1984
1985
        $maxDepth = 0;
1986
1987
        $subPath = $class;
1988
        while (false !== $lastPos = strrpos($subPath, '\\')) {
1989
            $subPath = substr($subPath, 0, $lastPos);
1990
            $search = $subPath . '\\';
1991
            if (isset($psr4_prefixes[$search])) {
1992
                $depth = substr_count($search, '\\');
1993
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
1994
1995
                foreach ($psr4_prefixes[$search] as $dir) {
1996
                    $dir = realpath($dir);
1997
1998
                    if ($dir
1999
                        && $depth > $maxDepth
2000
                        && $this->isInProjectDirs($dir . DIRECTORY_SEPARATOR . 'testdummy.php')
2001
                    ) {
2002
                        $maxDepth = $depth;
2003
                        $candidate_path = realpath($dir) . $pathEnd;
2004
                    }
2005
                }
2006
            }
2007
        }
2008
2009
        return $candidate_path;
2010
    }
2011
2012
    /**
2013
     * @param string $dir
2014
     *
2015
     * @return void
2016
     */
2017
    public static function removeCacheDirectory($dir)
2018
    {
2019
        if (is_dir($dir)) {
2020
            $objects = scandir($dir, SCANDIR_SORT_NONE);
2021
2022
            if ($objects === false) {
2023
                throw new \UnexpectedValueException('Not expecting false here');
2024
            }
2025
2026
            foreach ($objects as $object) {
2027
                if ($object != '.' && $object != '..') {
2028
                    if (filetype($dir . '/' . $object) == 'dir') {
2029
                        self::removeCacheDirectory($dir . '/' . $object);
2030
                    } else {
2031
                        unlink($dir . '/' . $object);
2032
                    }
2033
                }
2034
            }
2035
2036
            reset($objects);
2037
            rmdir($dir);
2038
        }
2039
    }
2040
2041
    /**
2042
     * @return void
2043
     */
2044
    public function setServerMode()
2045
    {
2046
        $this->cache_directory .= '-s';
2047
    }
2048
2049
    /** @return void */
2050
    public function addStubFile(string $stub_file)
2051
    {
2052
        $this->stub_files[$stub_file] = $stub_file;
2053
    }
2054
2055
    public function hasStubFile(string $stub_file) : bool
2056
    {
2057
        return isset($this->stub_files[$stub_file]);
2058
    }
2059
2060
    /**
2061
     * @return array<string, string>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

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

Loading history...
2062
     */
2063
    public function getStubFiles(): array
2064
    {
2065
        return $this->stub_files;
2066
    }
2067
2068
    public function getPhpVersion(): ?string
2069
    {
2070
        if (isset($this->configured_php_version)) {
2071
            return $this->configured_php_version;
2072
        }
2073
2074
        return $this->getPHPVersionFromComposerJson();
2075
    }
2076
2077
    private function setBooleanAttribute(string $name, bool $value): void
2078
    {
2079
        $this->$name = $value;
2080
    }
2081
2082
    /**
2083
     * @psalm-suppress MixedAssignment
2084
     * @psalm-suppress MixedArrayAccess
2085
     */
2086
    private function getPHPVersionFromComposerJson(): ?string
2087
    {
2088
        $composer_json_path = $this->base_dir . DIRECTORY_SEPARATOR. 'composer.json';
2089
2090
        if (file_exists($composer_json_path)) {
2091
            if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
2092
                throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path);
2093
            }
2094
            $php_version = $composer_json['require']['php'] ?? null;
2095
2096
            if (\is_string($php_version)) {
2097
                foreach (['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0'] as $candidate) {
2098
                    if (Semver::satisfies($candidate, $php_version)) {
2099
                        return $candidate;
2100
                    }
2101
                }
2102
            }
2103
        }
2104
        return null;
2105
    }
2106
}
2107