Test Setup Failed
Push — master ( b8c4ab...931d35 )
by Matthew
13:28 queued 09:02
created

Config::setIncludeCollector()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Psalm;
3
4
use Composer\Semver\Semver;
5
use Webmozart\PathUtil\Path;
6
use function array_merge;
7
use function array_pop;
8
use function class_exists;
9
use Composer\Autoload\ClassLoader;
10
use DOMDocument;
11
use LogicException;
12
13
use function count;
14
use const DIRECTORY_SEPARATOR;
15
use function dirname;
16
use const E_USER_ERROR;
17
use function explode;
18
use function file_exists;
19
use function file_get_contents;
20
use function filetype;
21
use function get_class;
22
use function get_defined_constants;
23
use function get_defined_functions;
24
use function glob;
25
use function in_array;
26
use function intval;
27
use function is_dir;
28
use function is_file;
29
use function json_decode;
30
use function libxml_clear_errors;
31
use const GLOB_NOSORT;
32
use const LIBXML_ERR_ERROR;
33
use const LIBXML_ERR_FATAL;
34
use function libxml_get_errors;
35
use function libxml_use_internal_errors;
36
use function mkdir;
37
use const PHP_EOL;
38
use function phpversion;
39
use function preg_match;
40
use function preg_quote;
41
use function preg_replace;
42
use Psalm\Config\IssueHandler;
43
use Psalm\Config\ProjectFileFilter;
44
use Psalm\Config\TaintAnalysisFileFilter;
45
use Psalm\Exception\ConfigException;
46
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
47
use Psalm\Internal\Analyzer\FileAnalyzer;
48
use Psalm\Internal\Analyzer\ProjectAnalyzer;
49
use Psalm\Internal\IncludeCollector;
50
use Psalm\Internal\Scanner\FileScanner;
51
use Psalm\Issue\ArgumentIssue;
52
use Psalm\Issue\ClassIssue;
53
use Psalm\Issue\CodeIssue;
54
use Psalm\Issue\FunctionIssue;
55
use Psalm\Issue\MethodIssue;
56
use Psalm\Issue\PropertyIssue;
57
use Psalm\Plugin\Hook;
58
use Psalm\Progress\Progress;
59
use Psalm\Progress\VoidProgress;
60
use function realpath;
61
use function reset;
62
use function rmdir;
63
use function scandir;
64
use function sha1;
65
use SimpleXMLElement;
66
use function strpos;
67
use function strrpos;
68
use function strtolower;
69
use function strtr;
70
use function substr;
71
use function substr_count;
72
use function sys_get_temp_dir;
73
use function trigger_error;
74
use function unlink;
75
use function version_compare;
76
use function getcwd;
77
use function chdir;
78
use function simplexml_import_dom;
79
use const LIBXML_NONET;
80
use function is_a;
81
use const SCANDIR_SORT_NONE;
82
83
/**
84
 * @psalm-suppress PropertyNotSetInConstructor
85
 */
86
class Config
87
{
88
    const DEFAULT_FILE_NAME = 'psalm.xml';
89
    const REPORT_INFO = 'info';
90
    const REPORT_ERROR = 'error';
91
    const REPORT_SUPPRESS = 'suppress';
92
93
    /**
94
     * @var array<string>
95
     */
96
    public static $ERROR_LEVELS = [
97
        self::REPORT_INFO,
98
        self::REPORT_ERROR,
99
        self::REPORT_SUPPRESS,
100
    ];
101
102
    /**
103
     * @var array
104
     */
105
    const MIXED_ISSUES = [
106
        'MixedArgument',
107
        'MixedArrayAccess',
108
        'MixedArrayAssignment',
109
        'MixedArrayOffset',
110
        'MixedArrayTypeCoercion',
111
        'MixedAssignment',
112
        'MixedFunctionCall',
113
        'MixedInferredReturnType',
114
        'MixedMethodCall',
115
        'MixedOperand',
116
        'MixedPropertyFetch',
117
        'MixedPropertyAssignment',
118
        'MixedReturnStatement',
119
        'MixedStringOffsetAssignment',
120
        'MixedTypeCoercion',
121
        'MixedArgumentTypeCoercion',
122
        'MixedPropertyTypeCoercion',
123
        'MixedReturnTypeCoercion',
124
    ];
125
126
    /**
127
     * @var static|null
128
     */
129
    private static $instance;
130
131
    /**
132
     * Whether or not to use types as defined in docblocks
133
     *
134
     * @var bool
135
     */
136
    public $use_docblock_types = true;
137
138
    /**
139
     * Whether or not to use types as defined in property docblocks.
140
     * This is distinct from the above because you may want to use
141
     * property docblocks, but not function docblocks.
142
     *
143
     * @var bool
144
     */
145
    public $use_docblock_property_types = true;
146
147
    /**
148
     * Whether or not to throw an exception on first error
149
     *
150
     * @var bool
151
     */
152
    public $throw_exception = false;
153
154
    /**
155
     * Whether or not to load Xdebug stub
156
     *
157
     * @var bool|null
158
     */
159
    public $load_xdebug_stub = null;
160
161
    /**
162
     * The directory to store PHP Parser (and other) caches
163
     *
164
     * @var string
165
     */
166
    public $cache_directory;
167
168
    /**
169
     * The directory to store all Psalm project caches
170
     *
171
     * @var string|null
172
     */
173
    public $global_cache_directory;
174
175
    /**
176
     * Path to the autoader
177
     *
178
     * @var string|null
179
     */
180
    public $autoloader;
181
182
    /**
183
     * @var ProjectFileFilter|null
184
     */
185
    protected $project_files;
186
187
    /**
188
     * @var ProjectFileFilter|null
189
     */
190
    protected $extra_files;
191
192
    /**
193
     * The base directory of this config file
194
     *
195
     * @var string
196
     */
197
    public $base_dir;
198
199
    /**
200
     * The PHP version to assume as declared in the config file
201
     *
202
     * @var string|null
203
     */
204
    private $configured_php_version;
205
206
    /**
207
     * @var array<int, string>
208
     */
209
    private $file_extensions = ['php'];
210
211
    /**
212
     * @var array<string, class-string<FileScanner>>
213
     */
214
    private $filetype_scanners = [];
215
216
    /**
217
     * @var array<string, class-string<FileAnalyzer>>
218
     */
219
    private $filetype_analyzers = [];
220
221
    /**
222
     * @var array<string, string>
223
     */
224
    private $filetype_scanner_paths = [];
225
226
    /**
227
     * @var array<string, string>
228
     */
229
    private $filetype_analyzer_paths = [];
230
231
    /**
232
     * @var array<string, IssueHandler>
233
     */
234
    private $issue_handlers = [];
235
236
    /**
237
     * @var array<int, string>
238
     */
239
    private $mock_classes = [];
240
241
    /**
242
     * @var array<string, string>
243
     */
244
    private $stub_files = [];
245
246
    /**
247
     * @var bool
248
     */
249
    public $hide_external_errors = false;
250
251
    /** @var bool */
252
    public $allow_includes = true;
253
254
    /** @var 1|2|3|4|5|6|7|8 */
255
    public $level = 1;
256
257
    /**
258
     * @var ?bool
259
     */
260
    public $show_mixed_issues = null;
261
262
    /** @var bool */
263
    public $strict_binary_operands = false;
264
265
    /** @var bool */
266
    public $add_void_docblocks = true;
267
268
    /**
269
     * If true, assert() calls can be used to check types of variables
270
     *
271
     * @var bool
272
     */
273
    public $use_assert_for_type = true;
274
275
    /**
276
     * @var bool
277
     */
278
    public $remember_property_assignments_after_call = true;
279
280
    /** @var bool */
281
    public $use_igbinary = false;
282
283
    /**
284
     * @var bool
285
     */
286
    public $allow_phpstorm_generics = false;
287
288
    /**
289
     * @var bool
290
     */
291
    public $allow_string_standin_for_class = false;
292
293
    /**
294
     * @var bool
295
     */
296
    public $use_phpdoc_method_without_magic_or_parent = false;
297
298
    /**
299
     * @var bool
300
     */
301
    public $use_phpdoc_property_without_magic_or_parent = false;
302
303
    /**
304
     * @var bool
305
     */
306
    public $skip_checks_on_unresolvable_includes = true;
307
308
    /**
309
     * @var bool
310
     */
311
    public $seal_all_methods = false;
312
313
    /**
314
     * @var bool
315
     */
316
    public $memoize_method_calls = false;
317
318
    /**
319
     * @var bool
320
     */
321
    public $hoist_constants = false;
322
323
    /**
324
     * @var bool
325
     */
326
    public $add_param_default_to_docblock_type = false;
327
328
    /**
329
     * @var bool
330
     */
331
    public $check_for_throws_docblock = false;
332
333
    /**
334
     * @var bool
335
     */
336
    public $check_for_throws_in_global_scope = false;
337
338
    /**
339
     * @var bool
340
     */
341
    public $ignore_internal_falsable_issues = true;
342
343
    /**
344
     * @var bool
345
     */
346
    public $ignore_internal_nullable_issues = true;
347
348
    /**
349
     * @var array<string, bool>
350
     */
351
    public $ignored_exceptions = [];
352
353
    /**
354
     * @var array<string, bool>
355
     */
356
    public $ignored_exceptions_in_global_scope = [];
357
358
    /**
359
     * @var array<string, bool>
360
     */
361
    public $ignored_exceptions_and_descendants = [];
362
363
    /**
364
     * @var array<string, bool>
365
     */
366
    public $ignored_exceptions_and_descendants_in_global_scope = [];
367
368
    /**
369
     * @var bool
370
     */
371
    public $infer_property_types_from_constructor = true;
372
373
    /**
374
     * @var bool
375
     */
376
    public $ensure_array_string_offsets_exist = false;
377
378
    /**
379
     * @var bool
380
     */
381
    public $ensure_array_int_offsets_exist = false;
382
383
    /**
384
     * @var array<string, bool>
385
     */
386
    public $forbidden_functions = [];
387
388
    /**
389
     * @var bool
390
     */
391
    public $forbid_echo = false;
392
393
    /**
394
     * @var bool
395
     */
396
    public $find_unused_code = false;
397
398
    /**
399
     * @var bool
400
     */
401
    public $find_unused_variables = false;
402
403
    /**
404
     * @var bool
405
     */
406
    public $run_taint_analysis = false;
407
408
    /**
409
     * Whether to resolve file and directory paths from the location of the config file,
410
     * instead of the current working directory.
411
     *
412
     * @var bool
413
     */
414
    public $resolve_from_config_file = false;
415
416
    /**
417
     * @var string[]
418
     */
419
    public $plugin_paths = [];
420
421
    /**
422
     * @var array<array{class:string,config:?SimpleXMLElement}>
423
     */
424
    private $plugin_classes = [];
425
426
    /**
427
     * Static methods to be called after method checks have completed
428
     *
429
     * @var class-string<Hook\AfterMethodCallAnalysisInterface>[]
430
     */
431
    public $after_method_checks = [];
432
433
    /**
434
     * Static methods to be called after project function checks have completed
435
     *
436
     * Called after function calls to functions defined in the project.
437
     *
438
     * Allows influencing the return type and adding of modifications.
439
     *
440
     * @var class-string<Hook\AfterFunctionCallAnalysisInterface>[]
441
     */
442
    public $after_function_checks = [];
443
444
    /**
445
     * Static methods to be called after every function call
446
     *
447
     * Called after each function call, including php internal functions.
448
     *
449
     * Cannot change the call or influence its return type
450
     *
451
     * @var class-string<Hook\AfterEveryFunctionCallAnalysisInterface>[]
452
     */
453
    public $after_every_function_checks = [];
454
455
456
    /**
457
     * Static methods to be called after expression checks have completed
458
     *
459
     * @var class-string<Hook\AfterExpressionAnalysisInterface>[]
460
     */
461
    public $after_expression_checks = [];
462
463
    /**
464
     * Static methods to be called after statement checks have completed
465
     *
466
     * @var class-string<Hook\AfterStatementAnalysisInterface>[]
467
     */
468
    public $after_statement_checks = [];
469
470
    /**
471
     * Static methods to be called after method checks have completed
472
     *
473
     * @var class-string<Hook\StringInterpreterInterface>[]
474
     */
475
    public $string_interpreters = [];
476
477
    /**
478
     * Static methods to be called after classlike exists checks have completed
479
     *
480
     * @var class-string<Hook\AfterClassLikeExistenceCheckInterface>[]
481
     */
482
    public $after_classlike_exists_checks = [];
483
484
    /**
485
     * Static methods to be called after classlike checks have completed
486
     *
487
     * @var class-string<Hook\AfterClassLikeAnalysisInterface>[]
488
     */
489
    public $after_classlike_checks = [];
490
491
    /**
492
     * Static methods to be called after classlikes have been scanned
493
     *
494
     * @var class-string<Hook\AfterClassLikeVisitInterface>[]
495
     */
496
    public $after_visit_classlikes = [];
497
498
    /**
499
     * Static methods to be called after codebase has been populated
500
     *
501
     * @var class-string<Hook\AfterCodebasePopulatedInterface>[]
502
     */
503
    public $after_codebase_populated = [];
504
505
    /**
506
     * Static methods to be called after codebase has been populated
507
     *
508
     * @var class-string<Hook\AfterAnalysisInterface>[]
509
     */
510
    public $after_analysis = [];
511
512
    /**
513
     * Static methods to be called after codebase has been populated
514
     * @var class-string<Hook\BeforeAnalyzeFileInterface>[]
515
     */
516
    public $before_analyze_file = [];
517
518
    /**
519
     * Static methods to be called after functionlike checks have completed
520
     *
521
     * @var class-string<Hook\AfterFunctionLikeAnalysisInterface>[]
522
     */
523
    public $after_functionlike_checks = [];
524
525
    /** @var array<string, mixed> */
526
    private $predefined_constants;
527
528
    /** @var array<callable-string, bool> */
529
    private $predefined_functions = [];
530
531
    /** @var ClassLoader|null */
532
    private $composer_class_loader;
533
534
    /**
535
     * Custom functions that always exit
536
     *
537
     * @var array<string, bool>
538
     */
539
    public $exit_functions = [];
540
541
    /**
542
     * @var string
543
     */
544
    public $hash = '';
545
546
    /** @var string|null */
547
    public $error_baseline = null;
548
549
    /**
550
     * @var bool
551
     */
552
    public $include_php_versions_in_error_baseline = false;
553
554
    /** @var string */
555
    public $shepherd_host = 'shepherd.dev';
556
557
    /**
558
     * @var array<string, string>
559
     */
560
    public $globals = [];
561
562
    /**
563
     * @var bool
564
     */
565
    public $parse_sql = false;
566
567
    /**
568
     * @var int
569
     */
570
    public $max_string_length = 1000;
571
572
    /** @var ?IncludeCollector */
573
    private $include_collector;
574
575
    /**
576
     * @var TaintAnalysisFileFilter|null
577
     */
578
    protected $taint_analysis_ignored_files;
579
580
    /**
581
     * @var bool whether to emit a backtrace of emitted issues to stderr
582
     */
583
    public $debug_emitted_issues = false;
584
585
    protected function __construct()
586
    {
587
        self::$instance = $this;
588
    }
589
590
    /**
591
     * Gets a Config object from an XML file.
592
     *
593
     * Searches up a folder hierarchy for the most immediate config.
594
     *
595
     * @param  string $path
596
     * @param  string $current_dir
597
     * @param  string $output_format
598
     *
599
     * @return Config
600
     * @throws ConfigException if a config path is not found
601
     *
602
     */
603
    public static function getConfigForPath($path, $current_dir, $output_format)
604
    {
605
        $config_path = self::locateConfigFile($path);
606
607
        if (!$config_path) {
608
            if ($output_format === \Psalm\Report::TYPE_CONSOLE) {
609
                echo 'Could not locate a config XML file in path ' . $path
610
                    . '. Have you run \'psalm --init\' ?' . PHP_EOL;
611
                exit(1);
612
            }
613
            throw new ConfigException('Config not found for path ' . $path);
614
        }
615
616
        return self::loadFromXMLFile($config_path, $current_dir);
617
    }
618
619
    /**
620
     * Searches up a folder hierarchy for the most immediate config.
621
     *
622
     * @throws ConfigException
623
     *
624
     * @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...
625
     */
626
    public static function locateConfigFile(string $path)
627
    {
628
        $dir_path = realpath($path);
629
630
        if ($dir_path === false) {
631
            throw new ConfigException('Config not found for path ' . $path);
632
        }
633
634
        if (!is_dir($dir_path)) {
635
            $dir_path = dirname($dir_path);
636
        }
637
638
        do {
639
            $maybe_path = $dir_path . DIRECTORY_SEPARATOR . Config::DEFAULT_FILE_NAME;
640
641
            if (file_exists($maybe_path) || file_exists($maybe_path .= '.dist')) {
642
                return $maybe_path;
643
            }
644
645
            $dir_path = dirname($dir_path);
646
        } while (dirname($dir_path) !== $dir_path);
647
648
        return null;
649
    }
650
651
    /**
652
     * Creates a new config object from the file
653
     *
654
     * @param  string           $file_path
655
     * @param  string           $current_dir
656
     *
657
     * @return self
658
     */
659
    public static function loadFromXMLFile($file_path, $current_dir)
660
    {
661
        $file_contents = file_get_contents($file_path);
662
663
        $base_dir = dirname($file_path) . DIRECTORY_SEPARATOR;
664
665
        if ($file_contents === false) {
666
            throw new \InvalidArgumentException('Cannot open ' . $file_path);
667
        }
668
669
        try {
670
            $config = self::loadFromXML($base_dir, $file_contents, $current_dir);
671
            $config->hash = sha1($file_contents . \PSALM_VERSION);
672
        } catch (ConfigException $e) {
673
            throw new ConfigException(
674
                'Problem parsing ' . $file_path . ":\n" . '  ' . $e->getMessage()
675
            );
676
        }
677
678
        return $config;
679
    }
680
681
    /**
682
     * Creates a new config object from an XML string
683
     *
684
     * @throws ConfigException
685
     *
686
     * @param  string           $base_dir
687
     * @param  string           $file_contents
688
     * @param  string|null      $current_dir Current working directory, if different to $base_dir
689
     *
690
     * @return self
691
     */
692
    public static function loadFromXML($base_dir, $file_contents, $current_dir = null)
693
    {
694
        if ($current_dir === null) {
695
            $current_dir = $base_dir;
696
        }
697
698
        self::validateXmlConfig($base_dir, $file_contents);
699
700
        return self::fromXmlAndPaths($base_dir, $file_contents, $current_dir);
701
    }
702
703
    private static function loadDomDocument(string $base_dir, string $file_contents): DOMDocument
704
    {
705
        $dom_document = new DOMDocument();
706
707
        // there's no obvious way to set xml:base for a document when loading it from string
708
        // so instead we're changing the current directory instead to be able to process XIncludes
709
        $oldpwd = getcwd();
710
        chdir($base_dir);
711
712
        $dom_document->loadXML($file_contents, LIBXML_NONET);
713
        $dom_document->xinclude(LIBXML_NONET);
714
715
        chdir($oldpwd);
716
        return $dom_document;
717
    }
718
719
    /**
720
     * @throws ConfigException
721
     */
722
    private static function validateXmlConfig(string $base_dir, string $file_contents): void
723
    {
724
        $schema_path = dirname(dirname(__DIR__)) . '/config.xsd';
725
726
        if (!file_exists($schema_path)) {
727
            throw new ConfigException('Cannot locate config schema');
728
        }
729
730
        $dom_document = self::loadDomDocument($base_dir, $file_contents);
731
732
        $psalm_nodes = $dom_document->getElementsByTagName('psalm');
733
734
        /** @var \DomElement|null */
735
        $psalm_node = $psalm_nodes->item(0);
736
737
        if (!$psalm_node) {
738
            throw new ConfigException(
739
                'Missing psalm node'
740
            );
741
        }
742
743
        if (!$psalm_node->hasAttribute('xmlns')) {
744
            $psalm_node->setAttribute('xmlns', 'https://getpsalm.org/schema/config');
745
746
            $old_dom_document = $dom_document;
747
            $dom_document = self::loadDomDocument($base_dir, $old_dom_document->saveXML());
748
        }
749
750
        // Enable user error handling
751
        libxml_use_internal_errors(true);
752
753
        if (!$dom_document->schemaValidate($schema_path)) {
754
            $errors = libxml_get_errors();
755
            foreach ($errors as $error) {
756
                if ($error->level === LIBXML_ERR_FATAL || $error->level === LIBXML_ERR_ERROR) {
757
                    throw new ConfigException(
758
                        'Error on line ' . $error->line . ":\n" . '    ' . $error->message
759
                    );
760
                }
761
            }
762
            libxml_clear_errors();
763
        }
764
    }
765
766
767
    /**
768
     * @psalm-suppress MixedMethodCall
769
     * @psalm-suppress MixedAssignment
770
     * @psalm-suppress MixedOperand
771
     * @psalm-suppress MixedArgument
772
     * @psalm-suppress MixedPropertyFetch
773
     *
774
     * @throws ConfigException
775
     */
776
    private static function fromXmlAndPaths(string $base_dir, string $file_contents, string $current_dir): self
777
    {
778
        $config = new static();
779
780
        $dom_document = self::loadDomDocument($base_dir, $file_contents);
781
782
        $config_xml = simplexml_import_dom($dom_document);
783
784
        $booleanAttributes = [
785
            'useDocblockTypes' => 'use_docblock_types',
786
            'useDocblockPropertyTypes' => 'use_docblock_property_types',
787
            'throwExceptionOnError' => 'throw_exception',
788
            'hideExternalErrors' => 'hide_external_errors',
789
            'resolveFromConfigFile' => 'resolve_from_config_file',
790
            'allowFileIncludes' => 'allow_includes',
791
            'strictBinaryOperands' => 'strict_binary_operands',
792
            'requireVoidReturnType' => 'add_void_docblocks',
793
            'useAssertForType' => 'use_assert_for_type',
794
            'rememberPropertyAssignmentsAfterCall' => 'remember_property_assignments_after_call',
795
            'allowPhpStormGenerics' => 'allow_phpstorm_generics',
796
            'allowStringToStandInForClass' => 'allow_string_standin_for_class',
797
            'usePhpDocMethodsWithoutMagicCall' => 'use_phpdoc_method_without_magic_or_parent',
798
            'usePhpDocPropertiesWithoutMagicCall' => 'use_phpdoc_property_without_magic_or_parent',
799
            'memoizeMethodCallResults' => 'memoize_method_calls',
800
            'hoistConstants' => 'hoist_constants',
801
            'addParamDefaultToDocblockType' => 'add_param_default_to_docblock_type',
802
            'checkForThrowsDocblock' => 'check_for_throws_docblock',
803
            'checkForThrowsInGlobalScope' => 'check_for_throws_in_global_scope',
804
            'forbidEcho' => 'forbid_echo',
805
            'ignoreInternalFunctionFalseReturn' => 'ignore_internal_falsable_issues',
806
            'ignoreInternalFunctionNullReturn' => 'ignore_internal_nullable_issues',
807
            'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline',
808
            'loadXdebugStub' => 'load_xdebug_stub',
809
            'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
810
            'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist',
811
            'reportMixedIssues' => 'show_mixed_issues',
812
            'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes',
813
            'sealAllMethods' => 'seal_all_methods',
814
            'runTaintAnalysis' => 'run_taint_analysis',
815
        ];
816
817
        foreach ($booleanAttributes as $xmlName => $internalName) {
818
            if (isset($config_xml[$xmlName])) {
819
                $attribute_text = (string) $config_xml[$xmlName];
820
                $config->setBooleanAttribute(
821
                    $internalName,
822
                    $attribute_text === 'true' || $attribute_text === '1'
823
                );
824
            }
825
        }
826
827
        if ($config->resolve_from_config_file) {
828
            $config->base_dir = $base_dir;
829
        } else {
830
            $config->base_dir = $current_dir;
831
            $base_dir = $current_dir;
832
        }
833
834
        if (isset($config_xml['phpVersion'])) {
835
            $config->configured_php_version = (string) $config_xml['phpVersion'];
836
        }
837
838
        if (isset($config_xml['autoloader'])) {
839
            $autoloader_path = $config->base_dir . DIRECTORY_SEPARATOR . $config_xml['autoloader'];
840
841
            if (!file_exists($autoloader_path)) {
842
                throw new ConfigException('Cannot locate autoloader');
843
            }
844
845
            $config->autoloader = realpath($autoloader_path);
846
        }
847
848
        if (isset($config_xml['cacheDirectory'])) {
849
            $config->cache_directory = (string)$config_xml['cacheDirectory'];
850
        } else {
851
            $config->cache_directory = sys_get_temp_dir() . '/psalm';
852
        }
853
854
        $config->global_cache_directory = $config->cache_directory;
855
856
        $config->cache_directory .= DIRECTORY_SEPARATOR . sha1($base_dir);
857
858
        if (is_dir($config->cache_directory) === false && @mkdir($config->cache_directory, 0777, true) === false) {
859
            trigger_error('Could not create cache directory: ' . $config->cache_directory, E_USER_ERROR);
860
        }
861
862
        if (isset($config_xml['serializer'])) {
863
            $attribute_text = (string) $config_xml['serializer'];
864
            $config->use_igbinary = $attribute_text === 'igbinary';
865
        } elseif ($igbinary_version = phpversion('igbinary')) {
866
            $config->use_igbinary = version_compare($igbinary_version, '2.0.5') >= 0;
867
        }
868
869
870
        if (isset($config_xml['findUnusedCode'])) {
871
            $attribute_text = (string) $config_xml['findUnusedCode'];
872
            $config->find_unused_code = $attribute_text === 'true' || $attribute_text === '1';
873
            $config->find_unused_variables = $config->find_unused_code;
874
        }
875
876
        if (isset($config_xml['findUnusedVariablesAndParams'])) {
877
            $attribute_text = (string) $config_xml['findUnusedVariablesAndParams'];
878
            $config->find_unused_variables = $attribute_text === 'true' || $attribute_text === '1';
879
        }
880
881
        if (isset($config_xml['errorLevel'])) {
882
            $attribute_text = (int) $config_xml['errorLevel'];
883
884
            if (!in_array($attribute_text, [1, 2, 3, 4, 5, 6, 7, 8], true)) {
885
                throw new Exception\ConfigException(
886
                    'Invalid error level ' . $config_xml['errorLevel']
887
                );
888
            }
889
890
            $config->level = $attribute_text;
891
        } elseif (isset($config_xml['totallyTyped'])) {
892
            $totally_typed = (string) $config_xml['totallyTyped'];
893
894
            if ($totally_typed === 'true' || $totally_typed === '1') {
895
                $config->level = 1;
896
            } else {
897
                $config->level = 2;
898
899
                if ($config->show_mixed_issues === null) {
900
                    $config->show_mixed_issues = false;
901
                }
902
            }
903
        } else {
904
            $config->level = 2;
905
        }
906
907
        if (isset($config_xml['errorBaseline'])) {
908
            $attribute_text = (string) $config_xml['errorBaseline'];
909
            $config->error_baseline = $attribute_text;
910
        }
911
912
        if (isset($config_xml['maxStringLength'])) {
913
            $attribute_text = intval($config_xml['maxStringLength']);
914
            $config->max_string_length = $attribute_text;
915
        }
916
917
        if (isset($config_xml['parseSql'])) {
918
            $attribute_text = (string) $config_xml['parseSql'];
919
            $config->parse_sql = $attribute_text === 'true' || $attribute_text === '1';
920
        }
921
922
        if (isset($config_xml['inferPropertyTypesFromConstructor'])) {
923
            $attribute_text = (string) $config_xml['inferPropertyTypesFromConstructor'];
924
            $config->infer_property_types_from_constructor = $attribute_text === 'true' || $attribute_text === '1';
925
        }
926
927
        if (isset($config_xml->projectFiles)) {
928
            $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true);
929
        }
930
931
        if (isset($config_xml->extraFiles)) {
932
            $config->extra_files = ProjectFileFilter::loadFromXMLElement($config_xml->extraFiles, $base_dir, true);
933
        }
934
935
        if (isset($config_xml->taintAnalysis->ignoreFiles)) {
936
            $config->taint_analysis_ignored_files = TaintAnalysisFileFilter::loadFromXMLElement(
937
                $config_xml->taintAnalysis->ignoreFiles,
938
                $base_dir,
939
                false
940
            );
941
        }
942
943
        if (isset($config_xml->fileExtensions)) {
944
            $config->file_extensions = [];
945
946
            $config->loadFileExtensions($config_xml->fileExtensions->extension);
947
        }
948
949
        if (isset($config_xml->mockClasses) && isset($config_xml->mockClasses->class)) {
950
            /** @var \SimpleXMLElement $mock_class */
951
            foreach ($config_xml->mockClasses->class as $mock_class) {
952
                $config->mock_classes[] = strtolower((string)$mock_class['name']);
953
            }
954
        }
955
956
        if (isset($config_xml->ignoreExceptions)) {
957
            if (isset($config_xml->ignoreExceptions->class)) {
958
                /** @var \SimpleXMLElement $exception_class */
959
                foreach ($config_xml->ignoreExceptions->class as $exception_class) {
960
                    $exception_name = (string) $exception_class['name'];
961
                    $global_attribute_text = (string) $exception_class['onlyGlobalScope'];
962
                    if ($global_attribute_text !== 'true' && $global_attribute_text !== '1') {
963
                        $config->ignored_exceptions[$exception_name] = true;
964
                    }
965
                    $config->ignored_exceptions_in_global_scope[$exception_name] = true;
966
                }
967
            }
968
            if (isset($config_xml->ignoreExceptions->classAndDescendants)) {
969
                /** @var \SimpleXMLElement $exception_class */
970
                foreach ($config_xml->ignoreExceptions->classAndDescendants as $exception_class) {
971
                    $exception_name = (string) $exception_class['name'];
972
                    $global_attribute_text = (string) $exception_class['onlyGlobalScope'];
973
                    if ($global_attribute_text !== 'true' && $global_attribute_text !== '1') {
974
                        $config->ignored_exceptions_and_descendants[$exception_name] = true;
975
                    }
976
                    $config->ignored_exceptions_and_descendants_in_global_scope[$exception_name] = true;
977
                }
978
            }
979
        }
980
981
        if (isset($config_xml->forbiddenFunctions) && isset($config_xml->forbiddenFunctions->function)) {
982
            /** @var \SimpleXMLElement $forbidden_function */
983
            foreach ($config_xml->forbiddenFunctions->function as $forbidden_function) {
984
                $config->forbidden_functions[strtolower((string) $forbidden_function['name'])] = true;
985
            }
986
        }
987
988
        if (isset($config_xml->exitFunctions) && isset($config_xml->exitFunctions->function)) {
989
            /** @var \SimpleXMLElement $exit_function */
990
            foreach ($config_xml->exitFunctions->function as $exit_function) {
991
                $config->exit_functions[strtolower((string) $exit_function['name'])] = true;
992
            }
993
        }
994
995
        if (isset($config_xml->stubs) && isset($config_xml->stubs->file)) {
996
            /** @var \SimpleXMLElement $stub_file */
997
            foreach ($config_xml->stubs->file as $stub_file) {
998
                $stub_file_name = (string)$stub_file['name'];
999
                if (!Path::isAbsolute($stub_file_name)) {
1000
                    $stub_file_name = $config->base_dir . DIRECTORY_SEPARATOR . $stub_file_name;
1001
                }
1002
                $file_path = realpath($stub_file_name);
1003
1004
                if (!$file_path) {
1005
                    throw new Exception\ConfigException(
1006
                        'Cannot resolve stubfile path ' . $config->base_dir . DIRECTORY_SEPARATOR . $stub_file['name']
1007
                    );
1008
                }
1009
1010
                $config->addStubFile($file_path);
1011
            }
1012
        }
1013
1014
        // this plugin loading system borrows heavily from etsy/phan
1015
        if (isset($config_xml->plugins)) {
1016
            if (isset($config_xml->plugins->plugin)) {
1017
                /** @var \SimpleXMLElement $plugin */
1018
                foreach ($config_xml->plugins->plugin as $plugin) {
1019
                    $plugin_file_name = (string) $plugin['filename'];
1020
1021
                    $path = Path::isAbsolute($plugin_file_name)
1022
                        ? $plugin_file_name
1023
                        : $config->base_dir . $plugin_file_name;
1024
1025
                    $config->addPluginPath($path);
1026
                }
1027
            }
1028
            if (isset($config_xml->plugins->pluginClass)) {
1029
                /** @var \SimpleXMLElement $plugin */
1030
                foreach ($config_xml->plugins->pluginClass as $plugin) {
1031
                    $plugin_class_name = $plugin['class'];
1032
                    // any child elements are used as plugin configuration
1033
                    $plugin_config = null;
1034
                    if ($plugin->count()) {
1035
                        $plugin_config = $plugin->children();
1036
                    }
1037
1038
                    $config->addPluginClass((string) $plugin_class_name, $plugin_config);
1039
                }
1040
            }
1041
        }
1042
1043
        if (isset($config_xml->issueHandlers)) {
1044
            /** @var \SimpleXMLElement $issue_handler */
1045
            foreach ($config_xml->issueHandlers->children() as $key => $issue_handler) {
1046
                if ($key === 'PluginIssue') {
1047
                    $custom_class_name = (string) $issue_handler['name'];
1048
                    /** @var string $key */
1049
                    $config->issue_handlers[$custom_class_name] = IssueHandler::loadFromXMLElement(
1050
                        $issue_handler,
1051
                        $base_dir
1052
                    );
1053
                } else {
1054
                    /** @var string $key */
1055
                    $config->issue_handlers[$key] = IssueHandler::loadFromXMLElement(
1056
                        $issue_handler,
1057
                        $base_dir
1058
                    );
1059
                }
1060
            }
1061
        }
1062
1063
        if (isset($config_xml->globals) && isset($config_xml->globals->var)) {
1064
            /** @var \SimpleXMLElement $var */
1065
            foreach ($config_xml->globals->var as $var) {
1066
                $config->globals['$' . (string) $var['name']] = (string) $var['type'];
1067
            }
1068
        }
1069
1070
        return $config;
1071
    }
1072
1073
    /**
1074
     * @return $this
1075
     */
1076
    public static function getInstance()
1077
    {
1078
        if (self::$instance) {
1079
            return self::$instance;
1080
        }
1081
1082
        throw new \UnexpectedValueException('No config initialized');
1083
    }
1084
1085
    /**
1086
     * @return void
1087
     */
1088
    public function setComposerClassLoader(?ClassLoader $loader = null)
1089
    {
1090
        $this->composer_class_loader = $loader;
1091
    }
1092
1093
    /**
1094
     * @param string $issue_key
1095
     * @param string $error_level
1096
     *
1097
     * @return void
1098
     */
1099
    public function setCustomErrorLevel($issue_key, $error_level)
1100
    {
1101
        $this->issue_handlers[$issue_key] = new IssueHandler();
1102
        $this->issue_handlers[$issue_key]->setErrorLevel($error_level);
1103
    }
1104
1105
    /**
1106
     * @param  array<SimpleXMLElement> $extensions
1107
     *
1108
     * @throws ConfigException if a Config file could not be found
1109
     *
1110
     * @return void
1111
     */
1112
    private function loadFileExtensions($extensions)
1113
    {
1114
        foreach ($extensions as $extension) {
1115
            $extension_name = preg_replace('/^\.?/', '', (string)$extension['name']);
1116
            $this->file_extensions[] = $extension_name;
1117
1118
            if (isset($extension['scanner'])) {
1119
                $path = $this->base_dir . (string)$extension['scanner'];
1120
1121
                if (!file_exists($path)) {
1122
                    throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
1123
                }
1124
1125
                $this->filetype_scanner_paths[$extension_name] = $path;
1126
            }
1127
1128
            if (isset($extension['checker'])) {
1129
                $path = $this->base_dir . (string)$extension['checker'];
1130
1131
                if (!file_exists($path)) {
1132
                    throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
1133
                }
1134
1135
                $this->filetype_analyzer_paths[$extension_name] = $path;
1136
            }
1137
        }
1138
    }
1139
1140
    /**
1141
     * @param string $path
1142
     *
1143
     * @return void
1144
     */
1145
    public function addPluginPath($path)
1146
    {
1147
        if (!file_exists($path)) {
1148
            throw new \InvalidArgumentException('Cannot find plugin file ' . $path);
1149
        }
1150
1151
        $this->plugin_paths[] = $path;
1152
    }
1153
1154
    /** @return void */
1155
    public function addPluginClass(string $class_name, SimpleXMLElement $plugin_config = null)
1156
    {
1157
        $this->plugin_classes[] = ['class' => $class_name, 'config' => $plugin_config];
1158
    }
1159
1160
    /** @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...
1161
    public function getPluginClasses(): array
1162
    {
1163
        return $this->plugin_classes;
1164
    }
1165
1166
    /**
1167
     * Initialises all the plugins (done once the config is fully loaded)
1168
     *
1169
     * @return void
1170
     * @psalm-suppress MixedAssignment
1171
     * @psalm-suppress MixedTypeCoercion
1172
     */
1173
    public function initializePlugins(ProjectAnalyzer $project_analyzer)
1174
    {
1175
        $codebase = $project_analyzer->getCodebase();
1176
1177
        $project_analyzer->progress->debug('Initializing plugins...' . PHP_EOL);
1178
1179
        $socket = new PluginRegistrationSocket($this, $codebase);
1180
        // initialize plugin classes earlier to let them hook into subsequent load process
1181
        foreach ($this->plugin_classes as $plugin_class_entry) {
1182
            $plugin_class_name = $plugin_class_entry['class'];
1183
            $plugin_config = $plugin_class_entry['config'];
1184
1185
            try {
1186
                // Below will attempt to load plugins from the project directory first.
1187
                // Failing that, it will use registered autoload chain, which will load
1188
                // plugins from Psalm directory or phar file. If that fails as well, it
1189
                // will fall back to project autoloader. It may seem that the last step
1190
                // will always fail, but it's only true if project uses Composer autoloader
1191
                if ($this->composer_class_loader
1192
                    && ($plugin_class_path = $this->composer_class_loader->findFile($plugin_class_name))
1193
                ) {
1194
                    $project_analyzer->progress->debug(
1195
                        'Loading plugin ' . $plugin_class_name . ' via require'. PHP_EOL
1196
                    );
1197
1198
                    /** @psalm-suppress UnresolvableInclude */
1199
                    require_once($plugin_class_path);
1200
                } else {
1201
                    if (!class_exists($plugin_class_name, true)) {
1202
                        throw new \UnexpectedValueException($plugin_class_name . ' is not a known class');
1203
                    }
1204
                }
1205
1206
                /**
1207
                 * @psalm-suppress InvalidStringClass
1208
                 *
1209
                 * @var Plugin\PluginEntryPointInterface
1210
                 */
1211
                $plugin_object = new $plugin_class_name;
1212
                $plugin_object($socket, $plugin_config);
1213
            } catch (\Throwable $e) {
1214
                throw new ConfigException('Failed to load plugin ' . $plugin_class_name, 0, $e);
1215
            }
1216
1217
            $project_analyzer->progress->debug('Loaded plugin ' . $plugin_class_name . ' successfully'. PHP_EOL);
1218
        }
1219
1220
        foreach ($this->filetype_scanner_paths as $extension => $path) {
1221
            $fq_class_name = $this->getPluginClassForPath(
1222
                $codebase,
1223
                $path,
1224
                FileScanner::class
1225
            );
1226
1227
            /** @psalm-suppress UnresolvableInclude */
1228
            require_once($path);
1229
1230
            $this->filetype_scanners[$extension] = $fq_class_name;
1231
        }
1232
1233
        foreach ($this->filetype_analyzer_paths as $extension => $path) {
1234
            $fq_class_name = $this->getPluginClassForPath(
1235
                $codebase,
1236
                $path,
1237
                FileAnalyzer::class
1238
            );
1239
1240
            /** @psalm-suppress UnresolvableInclude */
1241
            require_once($path);
1242
1243
            $this->filetype_analyzers[$extension] = $fq_class_name;
1244
        }
1245
1246
        foreach ($this->plugin_paths as $path) {
1247
            try {
1248
                $plugin_object = new FileBasedPluginAdapter($path, $this, $codebase);
1249
                $plugin_object($socket);
1250
            } catch (\Throwable $e) {
1251
                throw new ConfigException('Failed to load plugin ' . $path, 0, $e);
1252
            }
1253
        }
1254
    }
1255
1256
    /**
1257
     * @template T
1258
     *
1259
     * @param  string $path
1260
     * @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...
1261
     *
1262
     * @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...
1263
     */
1264
    private function getPluginClassForPath(Codebase $codebase, $path, $must_extend)
1265
    {
1266
        $file_storage = $codebase->createFileStorageForPath($path);
1267
        $file_to_scan = new FileScanner($path, $this->shortenFileName($path), true);
1268
        $file_to_scan->scan(
1269
            $codebase,
1270
            $file_storage
1271
        );
1272
1273
        $declared_classes = ClassLikeAnalyzer::getClassesForFile($codebase, $path);
1274
1275
        if (!count($declared_classes)) {
1276
            throw new \InvalidArgumentException(
1277
                'Plugins must have at least one class in the file - ' . $path . ' has ' .
1278
                    count($declared_classes)
1279
            );
1280
        }
1281
1282
        $fq_class_name = reset($declared_classes);
1283
1284
        if (!$codebase->classlikes->classExtends(
1285
            $fq_class_name,
1286
            $must_extend
1287
        )
1288
        ) {
1289
            throw new \InvalidArgumentException(
1290
                'This plugin must extend ' . $must_extend . ' - ' . $path . ' does not'
1291
            );
1292
        }
1293
1294
        /**
1295
         * @var class-string<T>
1296
         */
1297
        return $fq_class_name;
1298
    }
1299
1300
    /**
1301
     * @param  string $file_name
1302
     *
1303
     * @return string
1304
     */
1305
    public function shortenFileName($file_name)
1306
    {
1307
        return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $file_name);
1308
    }
1309
1310
    /**
1311
     * @param   string $issue_type
1312
     * @param   string $file_path
1313
     *
1314
     * @return  bool
1315
     */
1316
    public function reportIssueInFile($issue_type, $file_path)
1317
    {
1318
        if (($this->show_mixed_issues === false || $this->level > 2)
1319
            && in_array($issue_type, self::MIXED_ISSUES, true)
1320
        ) {
1321
            return false;
1322
        }
1323
1324
        if ($this->mustBeIgnored($file_path)) {
1325
            return false;
1326
        }
1327
1328
        $dependent_files = [strtolower($file_path) => $file_path];
1329
1330
        $project_analyzer = ProjectAnalyzer::getInstance();
1331
1332
        $codebase = $project_analyzer->getCodebase();
1333
1334
        if (!$this->hide_external_errors) {
1335
            try {
1336
                $file_storage = $codebase->file_storage_provider->get($file_path);
1337
                $dependent_files += $file_storage->required_by_file_paths;
1338
            } catch (\InvalidArgumentException $e) {
1339
                // do nothing
1340
            }
1341
        }
1342
1343
        $any_file_path_matched = false;
1344
1345
        foreach ($dependent_files as $dependent_file_path) {
1346
            if (((!$project_analyzer->full_run && $codebase->analyzer->canReportIssues($dependent_file_path))
1347
                    || $project_analyzer->canReportIssues($dependent_file_path))
1348
                && ($file_path === $dependent_file_path || !$this->mustBeIgnored($dependent_file_path))
1349
            ) {
1350
                $any_file_path_matched = true;
1351
                break;
1352
            }
1353
        }
1354
1355
        if (!$any_file_path_matched) {
1356
            return false;
1357
        }
1358
1359
        if ($this->getReportingLevelForFile($issue_type, $file_path) === self::REPORT_SUPPRESS) {
1360
            return false;
1361
        }
1362
1363
        return true;
1364
    }
1365
1366
    /**
1367
     * @param   string $file_path
1368
     *
1369
     * @return  bool
1370
     */
1371
    public function isInProjectDirs($file_path)
1372
    {
1373
        return $this->project_files && $this->project_files->allows($file_path);
1374
    }
1375
1376
    /**
1377
     * @param   string $file_path
1378
     *
1379
     * @return  bool
1380
     */
1381
    public function isInExtraDirs($file_path)
1382
    {
1383
        return $this->extra_files && $this->extra_files->allows($file_path);
1384
    }
1385
1386
    /**
1387
     * @param   string $file_path
1388
     *
1389
     * @return  bool
1390
     */
1391
    public function mustBeIgnored($file_path)
1392
    {
1393
        return $this->project_files && $this->project_files->forbids($file_path);
1394
    }
1395
1396
    public function trackTaintsInPath(string $file_path) : bool
1397
    {
1398
        return !$this->taint_analysis_ignored_files
1399
            || $this->taint_analysis_ignored_files->allows($file_path);
1400
    }
1401
1402
    public function getReportingLevelForIssue(CodeIssue $e) : string
1403
    {
1404
        $fqcn_parts = explode('\\', get_class($e));
1405
        $issue_type = array_pop($fqcn_parts);
1406
1407
        $reporting_level = null;
1408
1409
        if ($e instanceof ClassIssue) {
1410
            $reporting_level = $this->getReportingLevelForClass($issue_type, $e->fq_classlike_name);
1411
        } elseif ($e instanceof MethodIssue) {
1412
            $reporting_level = $this->getReportingLevelForMethod($issue_type, $e->method_id);
1413
        } elseif ($e instanceof FunctionIssue) {
1414
            $reporting_level = $this->getReportingLevelForFunction($issue_type, $e->function_id);
1415
        } elseif ($e instanceof PropertyIssue) {
1416
            $reporting_level = $this->getReportingLevelForProperty($issue_type, $e->property_id);
1417
        } elseif ($e instanceof ArgumentIssue && $e->function_id) {
1418
            $reporting_level = $this->getReportingLevelForArgument($issue_type, $e->function_id);
1419
        }
1420
1421
        if ($reporting_level === null) {
1422
            $reporting_level = $this->getReportingLevelForFile($issue_type, $e->getFilePath());
1423
        }
1424
1425
        $parent_issue_type = self::getParentIssueType($issue_type);
1426
1427
        if ($parent_issue_type && $reporting_level === Config::REPORT_ERROR) {
1428
            $parent_reporting_level = $this->getReportingLevelForFile($parent_issue_type, $e->getFilePath());
1429
1430
            if ($parent_reporting_level !== $reporting_level) {
1431
                return $parent_reporting_level;
1432
            }
1433
        }
1434
1435
        return $reporting_level;
1436
    }
1437
1438
    /**
1439
     * @param  string $issue_type
1440
     *
1441
     * @return string|null
1442
     */
1443
    public static function getParentIssueType($issue_type)
1444
    {
1445
        if ($issue_type === 'PossiblyUndefinedIntArrayOffset'
1446
            || $issue_type === 'PossiblyUndefinedStringArrayOffset'
1447
        ) {
1448
            return 'PossiblyUndefinedArrayOffset';
1449
        }
1450
1451
        if ($issue_type === 'PossiblyNullReference') {
1452
            return 'NullReference';
1453
        }
1454
1455
        if ($issue_type === 'PossiblyFalseReference') {
1456
            return null;
1457
        }
1458
1459
        if ($issue_type === 'PossiblyUndefinedArrayOffset') {
1460
            return null;
1461
        }
1462
1463
        if (strpos($issue_type, 'Possibly') === 0) {
1464
            $stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type);
1465
1466
            if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) {
1467
                $stripped_issue_type = 'Invalid' . $stripped_issue_type;
1468
            }
1469
1470
            return $stripped_issue_type;
1471
        }
1472
1473
        if (preg_match('/^(False|Null)[A-Z]/', $issue_type) && !strpos($issue_type, 'Reference')) {
1474
            return preg_replace('/^(False|Null)/', 'Invalid', $issue_type);
1475
        }
1476
1477
        if ($issue_type === 'UndefinedInterfaceMethod') {
1478
            return 'UndefinedMethod';
1479
        }
1480
1481
        if ($issue_type === 'UndefinedMagicPropertyFetch') {
1482
            return 'UndefinedPropertyFetch';
1483
        }
1484
1485
        if ($issue_type === 'UndefinedMagicPropertyAssignment') {
1486
            return 'UndefinedPropertyAssignment';
1487
        }
1488
1489
        if ($issue_type === 'UndefinedMagicMethod') {
1490
            return 'UndefinedMethod';
1491
        }
1492
1493
        if ($issue_type === 'PossibleRawObjectIteration') {
1494
            return 'RawObjectIteration';
1495
        }
1496
1497
        if ($issue_type === 'UninitializedProperty') {
1498
            return 'PropertyNotSetInConstructor';
1499
        }
1500
1501
        if ($issue_type === 'InvalidDocblockParamName') {
1502
            return 'InvalidDocblock';
1503
        }
1504
1505
        if ($issue_type === 'UnusedClosureParam') {
1506
            return 'UnusedParam';
1507
        }
1508
1509
        if ($issue_type === 'StringIncrement') {
1510
            return 'InvalidOperand';
1511
        }
1512
1513
        if ($issue_type === 'TraitMethodSignatureMismatch') {
1514
            return 'MethodSignatureMismatch';
1515
        }
1516
1517
        if ($issue_type === 'ImplementedParamTypeMismatch') {
1518
            return 'MoreSpecificImplementedParamType';
1519
        }
1520
1521
        if ($issue_type === 'UndefinedDocblockClass') {
1522
            return 'UndefinedClass';
1523
        }
1524
1525
        if ($issue_type === 'MixedArgumentTypeCoercion'
1526
            || $issue_type === 'MixedPropertyTypeCoercion'
1527
            || $issue_type === 'MixedReturnTypeCoercion'
1528
            || $issue_type === 'MixedArrayTypeCoercion'
1529
        ) {
1530
            return 'MixedTypeCoercion';
1531
        }
1532
1533
        if ($issue_type === 'ArgumentTypeCoercion'
1534
            || $issue_type === 'PropertyTypeCoercion'
1535
            || $issue_type === 'ReturnTypeCoercion'
1536
        ) {
1537
            return 'TypeCoercion';
1538
        }
1539
1540
        return null;
1541
    }
1542
1543
    /**
1544
     * @param   string $issue_type
1545
     * @param   string $file_path
1546
     *
1547
     * @return  string
1548
     */
1549
    public function getReportingLevelForFile($issue_type, $file_path)
1550
    {
1551
        if (isset($this->issue_handlers[$issue_type])) {
1552
            return $this->issue_handlers[$issue_type]->getReportingLevelForFile($file_path);
1553
        }
1554
1555
        // this string is replaced by scoper for Phars, so be careful
1556
        $issue_class = 'Psalm\\Issue\\' . $issue_type;
1557
1558
        if (!class_exists($issue_class) || !is_a($issue_class, \Psalm\Issue\CodeIssue::class, true)) {
1559
            return self::REPORT_ERROR;
1560
        }
1561
1562
        /** @var int */
1563
        $issue_level = $issue_class::ERROR_LEVEL;
1564
1565
        if ($issue_level > 0 && $issue_level < $this->level) {
1566
            return self::REPORT_INFO;
1567
        }
1568
1569
        return self::REPORT_ERROR;
1570
    }
1571
1572
    /**
1573
     * @param   string $issue_type
1574
     * @param   string $fq_classlike_name
1575
     *
1576
     * @return  string|null
1577
     */
1578
    public function getReportingLevelForClass($issue_type, $fq_classlike_name)
1579
    {
1580
        if (isset($this->issue_handlers[$issue_type])) {
1581
            return $this->issue_handlers[$issue_type]->getReportingLevelForClass($fq_classlike_name);
1582
        }
1583
    }
1584
1585
    /**
1586
     * @param   string $issue_type
1587
     * @param   string $method_id
1588
     *
1589
     * @return  string|null
1590
     */
1591
    public function getReportingLevelForMethod($issue_type, $method_id)
1592
    {
1593
        if (isset($this->issue_handlers[$issue_type])) {
1594
            return $this->issue_handlers[$issue_type]->getReportingLevelForMethod($method_id);
1595
        }
1596
    }
1597
1598
    /**
1599
     * @return  string|null
1600
     */
1601
    public function getReportingLevelForFunction(string $issue_type, string $function_id)
1602
    {
1603
        if (isset($this->issue_handlers[$issue_type])) {
1604
            return $this->issue_handlers[$issue_type]->getReportingLevelForFunction($function_id);
1605
        }
1606
    }
1607
1608
    /**
1609
     * @return  string|null
1610
     */
1611
    public function getReportingLevelForArgument(string $issue_type, string $function_id)
1612
    {
1613
        if (isset($this->issue_handlers[$issue_type])) {
1614
            return $this->issue_handlers[$issue_type]->getReportingLevelForArgument($function_id);
1615
        }
1616
    }
1617
1618
    /**
1619
     * @param   string $issue_type
1620
     * @param   string $property_id
1621
     *
1622
     * @return  string|null
1623
     */
1624
    public function getReportingLevelForProperty($issue_type, $property_id)
1625
    {
1626
        if (isset($this->issue_handlers[$issue_type])) {
1627
            return $this->issue_handlers[$issue_type]->getReportingLevelForProperty($property_id);
1628
        }
1629
    }
1630
1631
    /**
1632
     * @return array<string>
1633
     */
1634
    public function getProjectDirectories()
1635
    {
1636
        if (!$this->project_files) {
1637
            return [];
1638
        }
1639
1640
        return $this->project_files->getDirectories();
1641
    }
1642
1643
    /**
1644
     * @return array<string>
1645
     */
1646
    public function getProjectFiles()
1647
    {
1648
        if (!$this->project_files) {
1649
            return [];
1650
        }
1651
1652
        return $this->project_files->getFiles();
1653
    }
1654
1655
    /**
1656
     * @return array<string>
1657
     */
1658
    public function getExtraDirectories()
1659
    {
1660
        if (!$this->extra_files) {
1661
            return [];
1662
        }
1663
1664
        return $this->extra_files->getDirectories();
1665
    }
1666
1667
    /**
1668
     * @param   string $file_path
1669
     *
1670
     * @return  bool
1671
     */
1672
    public function reportTypeStatsForFile($file_path)
1673
    {
1674
        return $this->project_files
1675
            && $this->project_files->allows($file_path)
1676
            && $this->project_files->reportTypeStats($file_path);
1677
    }
1678
1679
    /**
1680
     * @param   string $file_path
1681
     *
1682
     * @return  bool
1683
     */
1684
    public function useStrictTypesForFile($file_path)
1685
    {
1686
        return $this->project_files && $this->project_files->useStrictTypes($file_path);
1687
    }
1688
1689
    /**
1690
     * @return array<string>
1691
     */
1692
    public function getFileExtensions()
1693
    {
1694
        return $this->file_extensions;
1695
    }
1696
1697
    /**
1698
     * @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...
1699
     */
1700
    public function getFiletypeScanners()
1701
    {
1702
        return $this->filetype_scanners;
1703
    }
1704
1705
    /**
1706
     * @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...
1707
     */
1708
    public function getFiletypeAnalyzers()
1709
    {
1710
        return $this->filetype_analyzers;
1711
    }
1712
1713
    /**
1714
     * @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...
1715
     */
1716
    public function getMockClasses()
1717
    {
1718
        return $this->mock_classes;
1719
    }
1720
1721
    /**
1722
     * @return void
1723
     */
1724
    public function visitStubFiles(Codebase $codebase, Progress $progress = null)
1725
    {
1726
        if ($progress === null) {
1727
            $progress = new VoidProgress();
1728
        }
1729
1730
        $codebase->register_stub_files = true;
1731
1732
        // note: don't realpath $generic_stubs_path, or phar version will fail
1733
        $generic_stubs_path = __DIR__ . '/Internal/Stubs/CoreGenericFunctions.phpstub';
1734
1735
        if (!file_exists($generic_stubs_path)) {
1736
            throw new \UnexpectedValueException('Cannot locate core generic stubs');
1737
        }
1738
1739
        // note: don't realpath $generic_classes_path, or phar version will fail
1740
        $generic_classes_path = __DIR__ . '/Internal/Stubs/CoreGenericClasses.phpstub';
1741
1742
        if (!file_exists($generic_classes_path)) {
1743
            throw new \UnexpectedValueException('Cannot locate core generic classes');
1744
        }
1745
1746
        // note: don't realpath $generic_classes_path, or phar version will fail
1747
        $immutable_classes_path = __DIR__ . '/Internal/Stubs/CoreImmutableClasses.phpstub';
1748
1749
        if (!file_exists($immutable_classes_path)) {
1750
            throw new \UnexpectedValueException('Cannot locate core immutable classes');
1751
        }
1752
1753
        $core_generic_files = [$generic_stubs_path, $generic_classes_path, $immutable_classes_path];
1754
1755
        if (\extension_loaded('ds')) {
1756
            $ext_ds_path = __DIR__ . '/Internal/Stubs/ext-ds.php';
1757
1758
            if (!file_exists($ext_ds_path)) {
1759
                throw new \UnexpectedValueException('Cannot locate core generic classes');
1760
            }
1761
1762
            $core_generic_files[] = $ext_ds_path;
1763
        }
1764
1765
        $stub_files = array_merge($core_generic_files, $this->stub_files);
1766
1767
        $phpstorm_meta_path = $this->base_dir . DIRECTORY_SEPARATOR . '.phpstorm.meta.php';
1768
1769
        if (is_file($phpstorm_meta_path)) {
1770
            $stub_files[] = $phpstorm_meta_path;
1771
        } elseif (is_dir($phpstorm_meta_path)) {
1772
            $phpstorm_meta_path = realpath($phpstorm_meta_path);
1773
1774
            foreach (glob($phpstorm_meta_path . '/*.meta.php', GLOB_NOSORT) as $glob) {
1775
                if (is_file($glob) && realpath(dirname($glob)) === $phpstorm_meta_path) {
1776
                    $stub_files[] = $glob;
1777
                }
1778
            }
1779
        }
1780
1781
        if ($this->load_xdebug_stub) {
1782
            $xdebug_stub_path = __DIR__ . '/Internal/Stubs/Xdebug.php';
1783
1784
            if (!file_exists($xdebug_stub_path)) {
1785
                throw new \UnexpectedValueException('Cannot locate XDebug stub');
1786
            }
1787
1788
            $stub_files[] = $xdebug_stub_path;
1789
        }
1790
1791
        foreach ($stub_files as $file_path) {
1792
            $file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
1793
            $codebase->scanner->addFileToDeepScan($file_path);
1794
        }
1795
1796
        $progress->debug('Registering stub files' . "\n");
1797
1798
        $codebase->scanFiles();
1799
1800
        $progress->debug('Finished registering stub files' . "\n");
1801
1802
        $codebase->register_stub_files = false;
1803
    }
1804
1805
    /**
1806
     * @return string
1807
     */
1808
    public function getCacheDirectory()
1809
    {
1810
        return $this->cache_directory;
1811
    }
1812
1813
    /**
1814
     * @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...
1815
     */
1816
    public function getGlobalCacheDirectory()
1817
    {
1818
        return $this->global_cache_directory;
1819
    }
1820
1821
    /**
1822
     * @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...
1823
     */
1824
    public function getPredefinedConstants()
1825
    {
1826
        return $this->predefined_constants;
1827
    }
1828
1829
    /**
1830
     * @return void
1831
     */
1832
    public function collectPredefinedConstants()
1833
    {
1834
        $this->predefined_constants = get_defined_constants();
1835
    }
1836
1837
    /**
1838
     * @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...
1839
     */
1840
    public function getPredefinedFunctions()
1841
    {
1842
        return $this->predefined_functions;
1843
    }
1844
1845
    /**
1846
     * @return void
1847
     */
1848
    public function collectPredefinedFunctions()
1849
    {
1850
        $defined_functions = get_defined_functions();
1851
1852
        if (isset($defined_functions['user'])) {
1853
            foreach ($defined_functions['user'] as $function_name) {
1854
                $this->predefined_functions[$function_name] = true;
1855
            }
1856
        }
1857
1858
        if (isset($defined_functions['internal'])) {
1859
            foreach ($defined_functions['internal'] as $function_name) {
1860
                $this->predefined_functions[$function_name] = true;
1861
            }
1862
        }
1863
    }
1864
1865
    public function setIncludeCollector(IncludeCollector $include_collector): void
1866
    {
1867
        $this->include_collector = $include_collector;
1868
    }
1869
1870
    /**
1871
     * @return void
1872
     *
1873
     * @psalm-suppress MixedAssignment
1874
     * @psalm-suppress MixedArrayAccess
1875
     */
1876
    public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, Progress $progress = null)
1877
    {
1878
        if ($progress === null) {
1879
            $progress = new VoidProgress();
1880
        }
1881
1882
        if (!$this->include_collector) {
1883
            throw new LogicException("IncludeCollector should be set at this point");
1884
        }
1885
1886
        $this->collectPredefinedConstants();
1887
        $this->collectPredefinedFunctions();
1888
1889
        $vendor_autoload_files_path
1890
            = $this->base_dir . DIRECTORY_SEPARATOR . 'vendor'
1891
                . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_files.php';
1892
1893
        if (file_exists($vendor_autoload_files_path)) {
1894
            $this->include_collector->runAndCollect(
1895
                function () use ($vendor_autoload_files_path) {
1896
                    /**
1897
                     * @psalm-suppress UnresolvableInclude
1898
                     * @var string[]
1899
                     */
1900
                    return require $vendor_autoload_files_path;
1901
                }
1902
            );
1903
        }
1904
1905
        $codebase = $project_analyzer->getCodebase();
1906
1907
        if ($this->autoloader) {
1908
            // somee classes that we think are missing may not actually be missing
1909
            // as they might be autoloadable once we require the autoloader below
1910
            $codebase->classlikes->forgetMissingClassLikes();
1911
1912
            $this->include_collector->runAndCollect(
1913
                function () {
1914
                    // do this in a separate method so scope does not leak
1915
                    /** @psalm-suppress UnresolvableInclude */
1916
                    require $this->autoloader;
1917
                }
1918
            );
1919
        }
1920
1921
        $autoload_included_files = $this->include_collector->getFilteredIncludedFiles();
1922
1923
        if ($autoload_included_files) {
1924
            $codebase->register_autoload_files = true;
1925
1926
            $progress->debug('Registering autoloaded files' . "\n");
1927
            foreach ($autoload_included_files as $file_path) {
1928
                $file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
1929
                $progress->debug('   ' . $file_path . "\n");
1930
                $codebase->scanner->addFileToDeepScan($file_path);
1931
            }
1932
1933
            $codebase->scanner->scanFiles($codebase->classlikes);
1934
1935
            $progress->debug('Finished registering autoloaded files' . "\n");
1936
1937
            $codebase->register_autoload_files = false;
1938
        }
1939
    }
1940
1941
    /**
1942
     * @param  string $fq_classlike_name
1943
     *
1944
     * @return string|false
1945
     */
1946
    public function getComposerFilePathForClassLike($fq_classlike_name)
1947
    {
1948
        if (!$this->composer_class_loader) {
1949
            return false;
1950
        }
1951
1952
        return $this->composer_class_loader->findFile($fq_classlike_name);
1953
    }
1954
1955
    public function getPotentialComposerFilePathForClassLike(string $class) : ?string
1956
    {
1957
        if (!$this->composer_class_loader) {
1958
            return null;
1959
        }
1960
1961
        /** @var array<string, array<int, string>> */
1962
        $psr4_prefixes = $this->composer_class_loader->getPrefixesPsr4();
1963
1964
        // PSR-4 lookup
1965
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';
1966
1967
        $candidate_path = null;
1968
1969
        $maxDepth = 0;
1970
1971
        $subPath = $class;
1972
        while (false !== $lastPos = strrpos($subPath, '\\')) {
1973
            $subPath = substr($subPath, 0, $lastPos);
1974
            $search = $subPath . '\\';
1975
            if (isset($psr4_prefixes[$search])) {
1976
                $depth = substr_count($search, '\\');
1977
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
1978
1979
                foreach ($psr4_prefixes[$search] as $dir) {
1980
                    $dir = realpath($dir);
1981
1982
                    if ($dir
1983
                        && $depth > $maxDepth
1984
                        && $this->isInProjectDirs($dir . DIRECTORY_SEPARATOR . 'testdummy.php')
1985
                    ) {
1986
                        $maxDepth = $depth;
1987
                        $candidate_path = realpath($dir) . $pathEnd;
1988
                    }
1989
                }
1990
            }
1991
        }
1992
1993
        return $candidate_path;
1994
    }
1995
1996
    /**
1997
     * @param string $dir
1998
     *
1999
     * @return void
2000
     */
2001
    public static function removeCacheDirectory($dir)
2002
    {
2003
        if (is_dir($dir)) {
2004
            $objects = scandir($dir, SCANDIR_SORT_NONE);
2005
2006
            if ($objects === false) {
2007
                throw new \UnexpectedValueException('Not expecting false here');
2008
            }
2009
2010
            foreach ($objects as $object) {
2011
                if ($object != '.' && $object != '..') {
2012
                    if (filetype($dir . '/' . $object) == 'dir') {
2013
                        self::removeCacheDirectory($dir . '/' . $object);
2014
                    } else {
2015
                        unlink($dir . '/' . $object);
2016
                    }
2017
                }
2018
            }
2019
2020
            reset($objects);
2021
            rmdir($dir);
2022
        }
2023
    }
2024
2025
    /**
2026
     * @return void
2027
     */
2028
    public function setServerMode()
2029
    {
2030
        $this->cache_directory .= '-s';
2031
    }
2032
2033
    /** @return void */
2034
    public function addStubFile(string $stub_file)
2035
    {
2036
        $this->stub_files[$stub_file] = $stub_file;
2037
    }
2038
2039
    public function hasStubFile(string $stub_file) : bool
2040
    {
2041
        return isset($this->stub_files[$stub_file]);
2042
    }
2043
2044
    /**
2045
     * @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...
2046
     */
2047
    public function getStubFiles(): array
2048
    {
2049
        return $this->stub_files;
2050
    }
2051
2052
    public function getPhpVersion(): ?string
2053
    {
2054
        if (isset($this->configured_php_version)) {
2055
            return $this->configured_php_version;
2056
        }
2057
2058
        return $this->getPHPVersionFromComposerJson();
2059
    }
2060
2061
    private function setBooleanAttribute(string $name, bool $value): void
2062
    {
2063
        $this->$name = $value;
2064
    }
2065
2066
    /**
2067
     * @psalm-suppress MixedAssignment
2068
     * @psalm-suppress MixedArrayAccess
2069
     */
2070
    private function getPHPVersionFromComposerJson(): ?string
2071
    {
2072
        $composer_json_path = $this->base_dir . DIRECTORY_SEPARATOR. 'composer.json';
2073
2074
        if (file_exists($composer_json_path)) {
2075
            if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
2076
                throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path);
2077
            }
2078
            $php_version = $composer_json['require']['php'] ?? null;
2079
2080
            if (\is_string($php_version)) {
2081
                foreach (['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0'] as $candidate) {
2082
                    if (Semver::satisfies($candidate, $php_version)) {
2083
                        return $candidate;
2084
                    }
2085
                }
2086
            }
2087
        }
2088
        return null;
2089
    }
2090
}
2091