Passed
Push — master ( cf7c5c...d67f1e )
by Matthew
07:04
created

Config::getVendorDir()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 18
Ratio 100 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 1
dl 18
loc 18
rs 9.2
c 0
b 0
f 0
1
<?php
2
namespace Psalm;
3
4
use Psalm\Checker\ClassLikeChecker;
5
use Psalm\Checker\ProjectChecker;
6
use Psalm\Config\IssueHandler;
7
use Psalm\Config\ProjectFileFilter;
8
use Psalm\Exception\ConfigException;
9
use Psalm\Scanner\FileScanner;
10
use SimpleXMLElement;
11
12
class Config
13
{
14
    const DEFAULT_FILE_NAME = 'psalm.xml';
15
    const REPORT_INFO = 'info';
16
    const REPORT_ERROR = 'error';
17
    const REPORT_SUPPRESS = 'suppress';
18
19
    /**
20
     * @var array<string>
21
     */
22
    public static $ERROR_LEVELS = [
23
        self::REPORT_INFO,
24
        self::REPORT_ERROR,
25
        self::REPORT_SUPPRESS,
26
    ];
27
28
    /**
29
     * @var array
30
     */
31
    protected static $MIXED_ISSUES = [
32
        'MixedArgument',
33
        'MixedArrayAccess',
34
        'MixedArrayAssignment',
35
        'MixedArrayOffset',
36
        'MixedAssignment',
37
        'MixedInferredReturnType',
38
        'MixedMethodCall',
39
        'MixedOperand',
40
        'MixedPropertyFetch',
41
        'MixedPropertyAssignment',
42
        'MixedReturnStatement',
43
        'MixedStringOffsetAssignment',
44
        'MixedTypeCoercion',
45
    ];
46
47
    /**
48
     * @var self|null
49
     */
50
    private static $instance;
51
52
    /**
53
     * Whether or not to use types as defined in docblocks
54
     *
55
     * @var bool
56
     */
57
    public $use_docblock_types = true;
58
59
    /**
60
     * Whether or not to use types as defined in property docblocks.
61
     * This is distinct from the above because you may want to use
62
     * property docblocks, but not function docblocks.
63
     *
64
     * @var bool
65
     */
66
    public $use_docblock_property_types = true;
67
68
    /**
69
     * Whether or not to throw an exception on first error
70
     *
71
     * @var bool
72
     */
73
    public $throw_exception = false;
74
75
    /**
76
     * The directory to store PHP Parser (and other) caches
77
     *
78
     * @var string
79
     */
80
    public $cache_directory;
81
82
    /**
83
     * Whether or not to care about casing of file names
84
     *
85
     * @var bool
86
     */
87
    public $use_case_sensitive_file_names = false;
88
89
    /**
90
     * Path to the autoader
91
     *
92
     * @var string|null
93
     */
94
    public $autoloader;
95
96
    /**
97
     * @var ProjectFileFilter|null
98
     */
99
    protected $project_files;
100
101
    /**
102
     * The base directory of this config file
103
     *
104
     * @var string
105
     */
106
    protected $base_dir;
107
108
    /**
109
     * @var array<int, string>
110
     */
111
    private $file_extensions = ['php'];
112
113
    /**
114
     * @var array<string, string>
115
     */
116
    private $filetype_scanners = [];
117
118
    /**
119
     * @var array<string, string>
120
     */
121
    private $filetype_checkers = [];
122
123
    /**
124
     * @var array<string, IssueHandler>
125
     */
126
    private $issue_handlers = [];
127
128
    /**
129
     * @var array<int, string>
130
     */
131
    private $mock_classes = [];
132
133
    /**
134
     * @var array<int, string>
135
     */
136
    private $stub_files = [];
137
138
    /**
139
     * @var bool
140
     */
141
    public $cache_file_hashes_during_run = true;
142
143
    /**
144
     * @var bool
145
     */
146
    public $hide_external_errors = true;
147
148
    /** @var bool */
149
    public $allow_includes = true;
150
151
    /** @var bool */
152
    public $totally_typed = false;
153
154
    /** @var bool */
155
    public $strict_binary_operands = false;
156
157
    /** @var bool */
158
    public $add_void_docblocks = true;
159
160
    /**
161
     * If true, assert() calls can be used to check types of variables
162
     *
163
     * @var bool
164
     */
165
    public $use_assert_for_type = false;
166
167
    /**
168
     * @var bool
169
     */
170
    public $remember_property_assignments_after_call = true;
171
172
    /** @var bool */
173
    public $use_igbinary = false;
174
175
    /**
176
     * Psalm plugins
177
     *
178
     * @var array<Plugin>
179
     */
180
    private $plugins = [];
181
182
    /** @var array<string, mixed> */
183
    private $predefined_constants;
184
185
    /** @var array<string, bool> */
186
    private $predefined_functions = [];
187
188
    protected function __construct()
189
    {
190
        self::$instance = $this;
191
    }
192
193
    /**
194
     * Gets a Config object from an XML file.
195
     *
196
     * Searches up a folder hierarchy for the most immediate config.
197
     *
198
     * @param  string $path
199
     * @param  string $base_dir
200
     * @param  string $output_format
201
     *
202
     * @throws ConfigException if a config path is not found
203
     *
204
     * @return Config
205
     */
206
    public static function getConfigForPath($path, $base_dir, $output_format)
207
    {
208
        $dir_path = realpath($path);
209
210
        if ($dir_path === false) {
211
            throw new ConfigException('Config not found for path ' . $path);
212
        }
213
214
        if (!is_dir($dir_path)) {
215
            $dir_path = dirname($dir_path);
216
        }
217
218
        $config = null;
219
220
        do {
221
            $maybe_path = $dir_path . DIRECTORY_SEPARATOR . Config::DEFAULT_FILE_NAME;
222
223
            if (file_exists($maybe_path)) {
224
                $config = self::loadFromXMLFile($maybe_path, $base_dir);
225
226
                break;
227
            }
228
229
            $dir_path = dirname($dir_path);
230
        } while (dirname($dir_path) !== $dir_path);
231
232
        if (!$config) {
233
            if ($output_format === ProjectChecker::TYPE_CONSOLE) {
234
                exit(
0 ignored issues
show
Coding Style Compatibility introduced by
The method getConfigForPath() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
235
                    'Could not locate a config XML file in path ' . $path . '. Have you run \'psalm --init\' ?' .
236
                    PHP_EOL
237
                );
238
            }
239
240
            throw new ConfigException('Config not found for path ' . $path);
241
        }
242
243
        return $config;
244
    }
245
246
    /**
247
     * Creates a new config object from the file
248
     *
249
     * @param  string           $file_path
250
     * @param  string           $base_dir
251
     *
252
     * @return self
253
     */
254
    public static function loadFromXMLFile($file_path, $base_dir)
255
    {
256
        $file_contents = file_get_contents($file_path);
257
258
        if ($file_contents === false) {
259
            throw new \InvalidArgumentException('Cannot open ' . $file_path);
260
        }
261
262
        return self::loadFromXML($file_path, $base_dir, $file_contents);
263
    }
264
265
    /**
266
     * Creates a new config object from an XML string
267
     *
268
     * @param  string           $file_path
269
     * @param  string           $base_dir
270
     * @param  string           $file_contents
271
     *
272
     * @return self
273
     * @psalm-suppress MixedArgument
274
     * @psalm-suppress MixedPropertyFetch
275
     * @psalm-suppress MixedMethodCall
276
     * @psalm-suppress MixedAssignment
277
     * @psalm-suppress MixedOperand
278
     * @psalm-suppress MixedPropertyAssignment
279
     */
280
    public static function loadFromXML($file_path, $base_dir, $file_contents)
281
    {
282
        $config = new static();
283
284
        $config->base_dir = $base_dir;
285
286
        $schema_path = dirname(dirname(__DIR__)) . '/config.xsd';
287
288
        if (!file_exists($schema_path)) {
289
            throw new ConfigException('Cannot locate config schema');
290
        }
291
292
        $dom_document = new \DOMDocument();
293
        $dom_document->loadXML($file_contents);
294
295
        // Enable user error handling
296
        libxml_use_internal_errors(true);
297
298
        if (!$dom_document->schemaValidate($schema_path)) {
299
            $errors = libxml_get_errors();
300
            foreach ($errors as $error) {
301
                if ($error->level === LIBXML_ERR_FATAL || $error->level === LIBXML_ERR_ERROR) {
302
                    throw new ConfigException(
303
                        'Error parsing file ' . $error->file . ' on line ' . $error->line . ': ' . $error->message
304
                    );
305
                }
306
            }
307
            libxml_clear_errors();
308
        }
309
310
        $config_xml = new SimpleXMLElement($file_contents);
311
312 View Code Duplication
        if (isset($config_xml['useDocblockTypes'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
313
            $attribute_text = (string) $config_xml['useDocblockTypes'];
314
            $config->use_docblock_types = $attribute_text === 'true' || $attribute_text === '1';
315
        }
316
317 View Code Duplication
        if (isset($config_xml['useDocblockPropertyTypes'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
318
            $attribute_text = (string) $config_xml['useDocblockPropertyTypes'];
319
            $config->use_docblock_property_types = $attribute_text === 'true' || $attribute_text === '1';
320
        }
321
322 View Code Duplication
        if (isset($config_xml['throwExceptionOnError'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
323
            $attribute_text = (string) $config_xml['throwExceptionOnError'];
324
            $config->throw_exception = $attribute_text === 'true' || $attribute_text === '1';
325
        }
326
327 View Code Duplication
        if (isset($config_xml['hideExternalErrors'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
328
            $attribute_text = (string) $config_xml['hideExternalErrors'];
329
            $config->hide_external_errors = $attribute_text === 'true' || $attribute_text === '1';
330
        }
331
332
        if (isset($config_xml['autoloader'])) {
333
            $config->autoloader = (string) $config_xml['autoloader'];
334
        }
335
336
        if (isset($config_xml['cacheDirectory'])) {
337
            $config->cache_directory = (string)$config_xml['cacheDirectory'];
338
        } else {
339
            $config->cache_directory = sys_get_temp_dir() . '/psalm';
340
        }
341
342
        if (@mkdir($config->cache_directory, 0777, true) === false && is_dir($config->cache_directory) === false) {
343
            trigger_error('Could not create cache directory: ' . $config->cache_directory, E_USER_ERROR);
344
        }
345
346 View Code Duplication
        if (isset($config_xml['allowFileIncludes'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
347
            $attribute_text = (string) $config_xml['allowFileIncludes'];
348
            $config->allow_includes = $attribute_text === 'true' || $attribute_text === '1';
349
        }
350
351 View Code Duplication
        if (isset($config_xml['totallyTyped'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
352
            $attribute_text = (string) $config_xml['totallyTyped'];
353
            $config->totally_typed = $attribute_text === 'true' || $attribute_text === '1';
354
        }
355
356 View Code Duplication
        if (isset($config_xml['strictBinaryOperands'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
357
            $attribute_text = (string) $config_xml['strictBinaryOperands'];
358
            $config->strict_binary_operands = $attribute_text === 'true' || $attribute_text === '1';
359
        }
360
361 View Code Duplication
        if (isset($config_xml['requireVoidReturnType'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
362
            $attribute_text = (string) $config_xml['requireVoidReturnType'];
363
            $config->add_void_docblocks = $attribute_text === 'true' || $attribute_text === '1';
364
        }
365
366 View Code Duplication
        if (isset($config_xml['useAssertForType'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
367
            $attribute_text = (string) $config_xml['useAssertForType'];
368
            $config->use_assert_for_type = $attribute_text === 'true' || $attribute_text === '1';
369
        }
370
371 View Code Duplication
        if (isset($config_xml['cacheFileContentHashes'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
372
            $attribute_text = (string) $config_xml['cacheFileContentHashes'];
373
            $config->cache_file_hashes_during_run = $attribute_text === 'true' || $attribute_text === '1';
374
        }
375
376 View Code Duplication
        if (isset($config_xml['rememberPropertyAssignmentsAfterCall'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
377
            $attribute_text = (string) $config_xml['rememberPropertyAssignmentsAfterCall'];
378
            $config->remember_property_assignments_after_call = $attribute_text === 'true' || $attribute_text === '1';
379
        }
380
381
        if (isset($config_xml['serializer'])) {
382
            $attribute_text = (string) $config_xml['serializer'];
383
            $config->use_igbinary = $attribute_text === 'igbinary';
384
        }
385
386
        if (isset($config_xml->projectFiles)) {
387
            $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true);
388
        }
389
390
        if (isset($config_xml->fileExtensions)) {
391
            $config->file_extensions = [];
392
393
            $config->loadFileExtensions($config_xml->fileExtensions->extension);
394
        }
395
396
        if (isset($config_xml->mockClasses) && isset($config_xml->mockClasses->class)) {
397
            /** @var \SimpleXMLElement $mock_class */
398
            foreach ($config_xml->mockClasses->class as $mock_class) {
399
                $config->mock_classes[] = (string)$mock_class['name'];
400
            }
401
        }
402
403
        if (isset($config_xml->stubs) && isset($config_xml->stubs->file)) {
404
            /** @var \SimpleXMLElement $stub_file */
405
            foreach ($config_xml->stubs->file as $stub_file) {
406
                $file_path = realpath($stub_file['name']);
407
408
                if (!$file_path) {
409
                    throw new Exception\ConfigException(
410
                        'Cannot resolve stubfile path ' . getcwd() . '/' . $stub_file['name']
411
                    );
412
                }
413
414
                $config->stub_files[] = $file_path;
415
            }
416
        }
417
418
        // this plugin loading system borrows heavily from etsy/phan
419
        if (isset($config_xml->plugins) && isset($config_xml->plugins->plugin)) {
420
            /** @var \SimpleXMLElement $plugin */
421
            foreach ($config_xml->plugins->plugin as $plugin) {
422
                $plugin_file_name = $plugin['filename'];
423
424
                $path = $config->base_dir . $plugin_file_name;
425
426
                $config->addPluginPath($path);
427
            }
428
        }
429
430
        if (isset($config_xml->issueHandlers)) {
431
            /** @var \SimpleXMLElement $issue_handler */
432
            foreach ($config_xml->issueHandlers->children() as $key => $issue_handler) {
433
                /** @var string $key */
434
                $config->issue_handlers[$key] = IssueHandler::loadFromXMLElement($issue_handler, $base_dir);
435
            }
436
        }
437
438
        if ($config->autoloader) {
439
            /** @psalm-suppress UnresolvableInclude */
440
            require_once($base_dir . DIRECTORY_SEPARATOR . $config->autoloader);
441
        }
442
443
        $config->collectPredefinedConstants();
444
        $config->collectPredefinedFunctions();
445
446
        return $config;
447
    }
448
449
    /**
450
     * @return $this
451
     */
452
    public static function getInstance()
453
    {
454
        if (self::$instance) {
455
            return self::$instance;
456
        }
457
458
        throw new \UnexpectedValueException('No config initialized');
459
    }
460
461
    /**
462
     * @param string $issue_key
463
     * @param string $error_level
464
     *
465
     * @return void
466
     */
467
    public function setCustomErrorLevel($issue_key, $error_level)
468
    {
469
        $this->issue_handlers[$issue_key] = new IssueHandler();
470
        $this->issue_handlers[$issue_key]->setErrorLevel($error_level);
471
    }
472
473
    /**
474
     * @param  array<SimpleXMLElement> $extensions
475
     *
476
     * @throws ConfigException if a Config file could not be found
477
     *
478
     * @return void
479
     */
480
    private function loadFileExtensions($extensions)
481
    {
482
        foreach ($extensions as $extension) {
483
            $extension_name = preg_replace('/^\.?/', '', (string)$extension['name']);
484
            $this->file_extensions[] = $extension_name;
485
486 View Code Duplication
            if (isset($extension['scanner'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
487
                $path = $this->base_dir . (string)$extension['scanner'];
488
489
                if (!file_exists($path)) {
490
                    throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
491
                }
492
493
                $this->filetype_scanners[$extension_name] = $path;
494
            }
495
496 View Code Duplication
            if (isset($extension['checker'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
497
                $path = $this->base_dir . (string)$extension['checker'];
498
499
                if (!file_exists($path)) {
500
                    throw new Exception\ConfigException('Error parsing config: cannot find file ' . $path);
501
                }
502
503
                $this->filetype_checkers[$extension_name] = $path;
504
            }
505
        }
506
    }
507
508
    /**
509
     * Initialises all the plugins (done once the config is fully loaded)
510
     *
511
     * @return void
512
     * @psalm-suppress MixedArrayAccess
513
     * @psalm-suppress MixedAssignment
514
     * @psalm-suppress MixedOperand
515
     */
516
    public function initializePlugins(ProjectChecker $project_checker)
517
    {
518
        $codebase = $project_checker->codebase;
519
520 View Code Duplication
        foreach ($this->filetype_scanners as &$path) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
521
            $file_storage = $codebase->createFileStorageForPath($path);
522
            $file_to_scan = new FileScanner($path, $this->shortenFileName($path), false);
523
            $file_to_scan->scan(
524
                $codebase,
525
                $codebase->getStatementsForFile($path),
526
                $file_storage
527
            );
528
529
            $declared_classes = ClassLikeChecker::getClassesForFile($project_checker, $path);
530
531
            if (count($declared_classes) !== 1) {
532
                throw new \InvalidArgumentException(
533
                    'Filetype handlers must have exactly one class in the file - ' . $path . ' has ' .
534
                        count($declared_classes)
535
                );
536
            }
537
538
            /** @psalm-suppress UnresolvableInclude */
539
            require_once($path);
540
541
            if (!\Psalm\Checker\ClassChecker::classExtends(
542
                $project_checker,
543
                $declared_classes[0],
544
                'Psalm\\Scanner\\FileScanner'
545
            )
546
            ) {
547
                throw new \InvalidArgumentException(
548
                    'Filetype handlers must extend \Psalm\Checker\FileChecker - ' . $path . ' does not'
549
                );
550
            }
551
552
            $path = $declared_classes[0];
553
        }
554
555 View Code Duplication
        foreach ($this->filetype_checkers as &$path) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
556
            $file_storage = $codebase->createFileStorageForPath($path);
557
            $file_to_scan = new FileScanner($path, $this->shortenFileName($path), false);
558
            $file_to_scan->scan(
559
                $codebase,
560
                $codebase->getStatementsForFile($path),
561
                $file_storage
562
            );
563
564
            $declared_classes = ClassLikeChecker::getClassesForFile($project_checker, $path);
565
566
            if (count($declared_classes) !== 1) {
567
                throw new \InvalidArgumentException(
568
                    'Filetype handlers must have exactly one class in the file - ' . $path . ' has ' .
569
                        count($declared_classes)
570
                );
571
            }
572
573
            /** @psalm-suppress UnresolvableInclude */
574
            require_once($path);
575
576
            if (!\Psalm\Checker\ClassChecker::classExtends(
577
                $project_checker,
578
                $declared_classes[0],
579
                'Psalm\\Checker\\FileChecker'
580
            )
581
            ) {
582
                throw new \InvalidArgumentException(
583
                    'Filetype handlers must extend \Psalm\Checker\FileChecker - ' . $path . ' does not'
584
                );
585
            }
586
587
            $path = $declared_classes[0];
588
        }
589
    }
590
591
    /**
592
     * @param  string $file_name
593
     *
594
     * @return string
595
     */
596
    public function shortenFileName($file_name)
597
    {
598
        return preg_replace('/^' . preg_quote($this->base_dir, DIRECTORY_SEPARATOR) . '/', '', $file_name);
599
    }
600
601
    /**
602
     * @param   string $issue_type
603
     * @param   string $file_path
604
     *
605
     * @return  bool
606
     */
607
    public function reportIssueInFile($issue_type, $file_path)
608
    {
609
        if (!$this->totally_typed && in_array($issue_type, self::$MIXED_ISSUES, true)) {
610
            return false;
611
        }
612
613
        if ($this->hide_external_errors) {
614
            $codebase = ProjectChecker::getInstance()->codebase;
615
616
            if (!$codebase->canReportIssues($file_path)) {
617
                return false;
618
            }
619
        }
620
621
        if ($this->getReportingLevelForFile($issue_type, $file_path) === self::REPORT_SUPPRESS) {
622
            return false;
623
        }
624
625
        return true;
626
    }
627
628
    /**
629
     * @param   string $file_path
630
     *
631
     * @return  bool
632
     */
633
    public function isInProjectDirs($file_path)
634
    {
635
        return $this->project_files && $this->project_files->allows($file_path);
636
    }
637
638
    /**
639
     * @param   string $issue_type
640
     * @param   string $file_path
641
     *
642
     * @return  string
643
     */
644
    public function getReportingLevelForFile($issue_type, $file_path)
645
    {
646
        if (isset($this->issue_handlers[$issue_type])) {
647
            return $this->issue_handlers[$issue_type]->getReportingLevelForFile($file_path);
648
        }
649
650
        return self::REPORT_ERROR;
651
    }
652
653
    /**
654
     * @return array<string>
655
     */
656
    public function getProjectDirectories()
657
    {
658
        if (!$this->project_files) {
659
            return [];
660
        }
661
662
        return $this->project_files->getDirectories();
663
    }
664
665
    /**
666
     * @return array<string>
667
     */
668
    public function getFileExtensions()
669
    {
670
        return $this->file_extensions;
671
    }
672
673
    /**
674
     * @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...
675
     */
676
    public function getFiletypeScanners()
677
    {
678
        return $this->filetype_scanners;
679
    }
680
681
    /**
682
     * @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...
683
     */
684
    public function getFiletypeCheckers()
685
    {
686
        return $this->filetype_checkers;
687
    }
688
689
    /**
690
     * @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...
691
     */
692
    public function getMockClasses()
693
    {
694
        return $this->mock_classes;
695
    }
696
697
    /**
698
     * @param  ProjectChecker $project_checker
0 ignored issues
show
Bug introduced by
There is no parameter named $project_checker. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
699
     *
700
     * @return void
701
     */
702
    public function visitStubFiles(Codebase $codebase)
703
    {
704
        $codebase->register_global_functions = true;
705
706
        $generic_stubs_path = realpath(__DIR__ . '/Stubs/CoreGenericFunctions.php');
707
708
        if (!$generic_stubs_path) {
709
            throw new \UnexpectedValueException('Cannot locate core generic stubs');
710
        }
711
712
        $file_storage = $codebase->createFileStorageForPath($generic_stubs_path);
713
        $file_to_scan = new FileScanner($generic_stubs_path, $this->shortenFileName($generic_stubs_path), false);
714
        $file_to_scan->scan(
715
            $codebase,
716
            $codebase->getStatementsForFile($generic_stubs_path),
717
            $file_storage
718
        );
719
720
        foreach ($this->stub_files as $stub_file_path) {
721
            $file_storage = $codebase->createFileStorageForPath($stub_file_path);
722
            $file_to_scan = new FileScanner($stub_file_path, $this->shortenFileName($stub_file_path), false);
723
            $file_to_scan->scan(
724
                $codebase,
725
                $codebase->getStatementsForFile($stub_file_path),
726
                $file_storage
727
            );
728
        }
729
730
        $codebase->register_global_functions = false;
731
    }
732
733
    /**
734
     * @return string
735
     */
736
    public function getCacheDirectory()
737
    {
738
        return $this->cache_directory;
739
    }
740
741
    /**
742
     * @return array<Plugin>
743
     */
744
    public function getPlugins()
745
    {
746
        return $this->plugins;
747
    }
748
749
    /**
750
     * @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...
751
     */
752
    public function getPredefinedConstants()
753
    {
754
        return $this->predefined_constants;
755
    }
756
757
    /**
758
     * @return void
759
     * @psalm-suppress MixedTypeCoercion
760
     */
761
    public function collectPredefinedConstants()
762
    {
763
        $this->predefined_constants = get_defined_constants();
764
    }
765
766
    /**
767
     * @return array<string, bool>
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...
768
     */
769
    public function getPredefinedFunctions()
770
    {
771
        return $this->predefined_functions;
772
    }
773
774
    /**
775
     * @return void
776
     * @psalm-suppress InvalidPropertyAssignment
777
     * @psalm-suppress MixedAssignment
778
     * @psalm-suppress MixedArrayOffset
779
     */
780
    public function collectPredefinedFunctions()
781
    {
782
        $defined_functions = get_defined_functions();
783
784 View Code Duplication
        if (isset($defined_functions['user'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
785
            foreach ($defined_functions['user'] as $function_name) {
786
                $this->predefined_functions[$function_name] = true;
787
            }
788
        }
789
790 View Code Duplication
        if (isset($defined_functions['internal'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
791
            foreach ($defined_functions['internal'] as $function_name) {
792
                $this->predefined_functions[$function_name] = true;
793
            }
794
        }
795
    }
796
797
    /**
798
     * @return void
799
     *
800
     * @psalm-suppress MixedAssignment
801
     * @psalm-suppress MixedArrayAccess
802
     */
803
    public function visitComposerAutoloadFiles(ProjectChecker $project_checker)
804
    {
805
        $composer_json_path = $this->base_dir . 'composer.json'; // this should ideally not be hardcoded
806
807
        if (!file_exists($composer_json_path)) {
808
            return;
809
        }
810
811
        /** @psalm-suppress PossiblyFalseArgument */
812
        if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
813
            throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path);
814
        }
815
816
        if (isset($composer_json['autoload']['files'])) {
817
            $codebase = $project_checker->codebase;
818
            $codebase->register_global_functions = true;
819
820
            /** @var string[] */
821
            $files = $composer_json['autoload']['files'];
822
823
            foreach ($files as $file) {
824
                $file_path = realpath($this->base_dir . $file);
825
826
                if (!$file_path) {
827
                    continue;
828
                }
829
830
                $file_storage = $codebase->createFileStorageForPath($file_path);
831
                $file_to_scan = new \Psalm\Scanner\FileScanner($file_path, $this->shortenFileName($file_path), false);
832
                $file_to_scan->scan(
833
                    $codebase,
834
                    $codebase->getStatementsForFile($file_path),
835
                    $file_storage
836
                );
837
            }
838
839
            $project_checker->codebase->register_global_functions = false;
840
        }
841
    }
842
843
    /**
844
     * @param  string $current_dir
845
     *
846
     * @return string
847
     *
848
     * @psalm-suppress PossiblyFalseArgument
849
     * @psalm-suppress MixedArrayAccess
850
     * @psalm-suppress MixedAssignment
851
     */
852 View Code Duplication
    private static function getVendorDir($current_dir)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
853
    {
854
        $composer_json_path = $current_dir . DIRECTORY_SEPARATOR . 'composer.json'; // this should ideally not be hardcoded
855
856
        if (!file_exists($composer_json_path)) {
857
            return 'vendor';
858
        }
859
860
        if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
861
            throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path);
862
        }
863
864
        if (isset($composer_json['config']['vendor-dir'])) {
865
             return (string) $composer_json['config']['vendor-dir'];
866
        }
867
868
        return 'vendor';
869
    }
870
871
    /**
872
     * @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...
873
     */
874
    public function getComposerClassMap()
875
    {
876
        $vendor_dir = realpath($this->base_dir . self::getVendorDir($this->base_dir));
877
878
        if (!$vendor_dir) {
879
            return [];
880
        }
881
882
        $autoload_files_classmap =
883
            $vendor_dir . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_classmap.php';
884
885
        if (!file_exists($autoload_files_classmap)) {
886
            return [];
887
        }
888
889
        /**
890
         * @psalm-suppress MixedAssignment
891
         * @psalm-suppress UnresolvableInclude
892
         */
893
        $class_map = include_once $autoload_files_classmap;
894
895
        if (is_array($class_map)) {
896
            $composer_classmap = array_change_key_case($class_map);
897
898
            $composer_classmap = array_filter(
899
                $composer_classmap,
900
                /**
901
                 * @param string $file_path
902
                 *
903
                 * @return bool
904
                 */
905
                function ($file_path) use ($vendor_dir) {
906
                    return strpos($file_path, $vendor_dir) === 0;
907
                }
908
            );
909
        } else {
910
            $composer_classmap = [];
911
        }
912
913
        return $composer_classmap;
914
    }
915
916
    /**
917
     * @param string $dir
918
     *
919
     * @return void
920
     */
921
    public static function removeCacheDirectory($dir)
922
    {
923
        if (is_dir($dir)) {
924
            $objects = scandir($dir);
925
926
            if ($objects === false) {
927
                throw new \UnexpectedValueException('Not expecting false here');
928
            }
929
930
            foreach ($objects as $object) {
931
                if ($object != '.' && $object != '..') {
932
                    if (filetype($dir . '/' . $object) == 'dir') {
933
                        self::removeCacheDirectory($dir . '/' . $object);
934
                    } else {
935
                        unlink($dir . '/' . $object);
936
                    }
937
                }
938
            }
939
940
            reset($objects);
941
            rmdir($dir);
942
        }
943
    }
944
945
    /**
946
     * @param string $path
947
     *
948
     * @return void
949
     */
950
    public function addPluginPath($path)
951
    {
952
        if (!file_exists($path)) {
953
            throw new \InvalidArgumentException('Cannot find file ' . $path);
954
        }
955
956
        /**
957
         * @var Plugin
958
         * @psalm-suppress UnresolvableInclude
959
         */
960
        $loaded_plugin = require_once($path);
961
962
        if (!$loaded_plugin) {
963
            throw new \InvalidArgumentException(
964
                'Plugins must return an instance of that plugin at the end of the file - ' .
965
                    $plugin_file_name . ' does not'
966
            );
967
        }
968
969
        if (!($loaded_plugin instanceof Plugin)) {
970
            throw new \InvalidArgumentException(
971
                'Plugins must extend \Psalm\Plugin - ' . $path . ' does not'
972
            );
973
        }
974
975
        $this->plugins[] = $loaded_plugin;
976
    }
977
}
978