Passed
Pull Request — master (#861)
by SignpostMarv
06:33 queued 02:07
created

ProjectChecker::disableCheckerCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Psalm\Checker;
3
4
use Psalm\Codebase;
5
use Psalm\Config;
6
use Psalm\Context;
7
use Psalm\Provider\ClassLikeStorageCacheProvider;
8
use Psalm\Provider\ClassLikeStorageProvider;
9
use Psalm\Provider\FileProvider;
10
use Psalm\Provider\FileReferenceProvider;
11
use Psalm\Provider\FileStorageCacheProvider;
12
use Psalm\Provider\FileStorageProvider;
13
use Psalm\Provider\ParserCacheProvider;
14
use Psalm\Provider\StatementsProvider;
15
use Psalm\Type;
16
use RecursiveDirectoryIterator;
17
use RecursiveIteratorIterator;
18
19
class ProjectChecker
20
{
21
    /**
22
     * Cached config
23
     *
24
     * @var Config
25
     */
26
    public $config;
27
28
    /**
29
     * @var self
30
     */
31
    public static $instance;
32
33
    /**
34
     * An object representing everything we know about the code
35
     *
36
     * @var Codebase
37
     */
38
    public $codebase;
39
40
    /** @var FileProvider */
41
    private $file_provider;
42
43
    /** @var FileStorageProvider */
44
    public $file_storage_provider;
45
46
    /** @var ClassLikeStorageProvider */
47
    public $classlike_storage_provider;
48
49
    /** @var ParserCacheProvider */
50
    public $cache_provider;
51
52
    /**
53
     * Whether or not to use colors in error output
54
     *
55
     * @var bool
56
     */
57
    public $use_color;
58
59
    /**
60
     * Whether or not to show snippets in error output
61
     *
62
     * @var bool
63
     */
64
    public $show_snippet;
65
66
    /**
67
     * Whether or not to show informational messages
68
     *
69
     * @var bool
70
     */
71
    public $show_info;
72
73
    /**
74
     * @var string
75
     */
76
    public $output_format;
77
78
    /**
79
     * @var bool
80
     */
81
    public $debug_output = false;
82
83
    /**
84
     * @var bool
85
     */
86
    public $debug_lines = false;
87
88
    /**
89
     * @var bool
90
     */
91
    public $alter_code = false;
92
93
    /**
94
     * @var bool
95
     */
96
    public $show_issues = true;
97
98
    /** @var int */
99
    public $threads;
100
101
    /**
102
     * Whether or not to infer types from usage. Computationally expensive, so turned off by default
103
     *
104
     * @var bool
105
     */
106
    public $infer_types_from_usage = false;
107
108
    /**
109
     * @var array<string,string>
110
     */
111
    public $reports = [];
112
113
    /**
114
     * @var array<string, bool>
115
     */
116
    private $issues_to_fix = [];
117
118
    /**
119
     * @var int
120
     */
121
    public $php_major_version = PHP_MAJOR_VERSION;
122
123
    /**
124
     * @var int
125
     */
126
    public $php_minor_version = PHP_MINOR_VERSION;
127
128
    /**
129
     * @var bool
130
     */
131
    public $dry_run = false;
132
133
    /**
134
     * @var bool
135
     */
136
    public $only_replace_php_types_with_non_docblock_types = false;
137
138
    /**
139
     * @var array<string, FileChecker>
140
     */
141
    private $file_checkers = [];
142
143
    /**
144
     * @var bool
145
     */
146
    private $cache = false;
147
148
    const TYPE_CONSOLE = 'console';
149
    const TYPE_PYLINT = 'pylint';
150
    const TYPE_JSON = 'json';
151
    const TYPE_EMACS = 'emacs';
152
    const TYPE_XML = 'xml';
153
154
    const SUPPORTED_OUTPUT_TYPES = [
155
        self::TYPE_CONSOLE,
156
        self::TYPE_PYLINT,
157
        self::TYPE_JSON,
158
        self::TYPE_EMACS,
159
        self::TYPE_XML,
160
    ];
161
162
    /**
163
     * @param FileProvider  $file_provider
164
     * @param ParserCacheProvider $cache_provider
165
     * @param bool          $use_color
166
     * @param bool          $show_info
167
     * @param string        $output_format
168
     * @param int           $threads
169
     * @param bool          $debug_output
170
     * @param string        $reports
171
     * @param bool          $show_snippet
172
     */
173
    public function __construct(
174
        Config $config,
175
        FileProvider $file_provider,
176
        ParserCacheProvider $cache_provider,
177
        FileStorageCacheProvider $file_storage_cache_provider,
178
        ClassLikeStorageCacheProvider $classlike_storage_cache_provider,
179
        $use_color = true,
180
        $show_info = true,
181
        $output_format = self::TYPE_CONSOLE,
182
        $threads = 1,
183
        $debug_output = false,
184
        $reports = null,
185
        $show_snippet = true
186
    ) {
187
        $this->file_provider = $file_provider;
188
        $this->cache_provider = $cache_provider;
189
        $this->use_color = $use_color;
190
        $this->show_info = $show_info;
191
        $this->debug_output = $debug_output;
192
        $this->threads = $threads;
193
        $this->config = $config;
194
        $this->show_snippet = $show_snippet;
195
196
        $this->file_storage_provider = new FileStorageProvider($file_storage_cache_provider);
197
        $this->classlike_storage_provider = new ClassLikeStorageProvider($classlike_storage_cache_provider);
198
199
        $statements_provider = new StatementsProvider(
200
            $file_provider,
201
            $cache_provider,
202
            $file_storage_cache_provider
203
        );
204
205
        $this->codebase = new Codebase(
206
            $config,
207
            $this->file_storage_provider,
208
            $this->classlike_storage_provider,
209
            $file_provider,
210
            $statements_provider,
211
            $debug_output
212
        );
213
214
        if (!in_array($output_format, self::SUPPORTED_OUTPUT_TYPES, true)) {
215
            throw new \UnexpectedValueException('Unrecognised output format ' . $output_format);
216
        }
217
218
        if ($reports) {
219
            /**
220
             * @var array<string,string>
221
             */
222
            $mapping = [
223
                '.xml' => self::TYPE_XML,
224
                '.json' => self::TYPE_JSON,
225
                '.txt' => self::TYPE_EMACS,
226
                '.emacs' => self::TYPE_EMACS,
227
                '.pylint' => self::TYPE_PYLINT,
228
            ];
229
            foreach ($mapping as $extension => $type) {
230
                if (substr($reports, -strlen($extension)) === $extension) {
231
                    $this->reports[$type] = $reports;
232
                    break;
233
                }
234
            }
235
            if (empty($this->reports)) {
236
                throw new \UnexpectedValueException('Unrecognised report format ' . $reports);
237
            }
238
        }
239
240
        $this->output_format = $output_format;
241
        self::$instance = $this;
242
243
        $this->cache_provider->use_igbinary = $config->use_igbinary;
244
    }
245
246
    /**
247
     * @return ProjectChecker
248
     */
249
    public static function getInstance()
250
    {
251
        return self::$instance;
252
    }
253
254
    /**
255
     * @param  string  $base_dir
256
     * @param  bool $is_diff
257
     *
258
     * @return void
259
     */
260
    public function check($base_dir, $is_diff = false)
261
    {
262
        $start_checks = (int)microtime(true);
263
264
        if (!$base_dir) {
265
            throw new \InvalidArgumentException('Cannot work with empty base_dir');
266
        }
267
268
        $diff_files = null;
269
        $deleted_files = null;
270
271
        if ($is_diff && FileReferenceProvider::loadReferenceCache() && $this->cache_provider->canDiffFiles()) {
272
            $deleted_files = FileReferenceProvider::getDeletedReferencedFiles();
273
            $diff_files = $deleted_files;
274
275
            foreach ($this->config->getProjectDirectories() as $dir_name) {
276
                $diff_files = array_merge($diff_files, $this->getDiffFilesInDir($dir_name, $this->config));
277
            }
278
        }
279
280
        if ($this->output_format === self::TYPE_CONSOLE) {
281
            echo 'Scanning files...' . "\n";
282
        }
283
284
        if ($diff_files === null || $deleted_files === null || count($diff_files) > 200) {
285
            foreach ($this->config->getProjectDirectories() as $dir_name) {
286
                $this->checkDirWithConfig($dir_name, $this->config);
287
            }
288
289
            foreach ($this->config->getProjectFiles() as $file_path) {
290
                $this->codebase->addFilesToAnalyze([$file_path => $file_path]);
291
            }
292
293
            $this->config->initializePlugins($this);
294
295
            $this->codebase->scanFiles();
296
        } else {
297
            if ($this->debug_output) {
298
                echo count($diff_files) . ' changed files' . "\n";
299
            }
300
301
            if ($diff_files) {
302
                $file_list = self::getReferencedFilesFromDiff($diff_files);
303
304
                // strip out deleted files
305
                $file_list = array_diff($file_list, $deleted_files);
306
307
                $this->checkDiffFilesWithConfig($this->config, $file_list);
308
309
                $this->config->initializePlugins($this);
310
311
                $this->codebase->scanFiles();
312
            }
313
        }
314
315
        if ($this->output_format === self::TYPE_CONSOLE) {
316
            echo 'Analyzing files...' . "\n";
317
        }
318
319
        $this->config->visitStubFiles($this->codebase, $this->debug_output);
320
321
        $this->codebase->analyzer->analyzeFiles($this, $this->threads, $this->alter_code);
322
323
        $removed_parser_files = $this->cache_provider->deleteOldParserCaches(
324
            $is_diff ? $this->cache_provider->getLastGoodRun() : $start_checks
325
        );
326
327
        if ($this->debug_output && $removed_parser_files) {
328
            echo 'Removed ' . $removed_parser_files . ' old parser caches' . "\n";
329
        }
330
331
        if ($is_diff) {
332
            $this->cache_provider->touchParserCaches($this->getAllFiles($this->config), $start_checks);
333
        }
334
    }
335
336
    /**
337
     * @return void
338
     */
339
    public function checkClassReferences()
340
    {
341
        if (!$this->codebase->collect_references) {
342
            throw new \UnexpectedValueException('Should not be checking references');
343
        }
344
345
        $this->codebase->classlikes->checkClassReferences();
346
    }
347
348
    /**
349
     * @param  string $symbol
350
     *
351
     * @return void
352
     */
353
    public function findReferencesTo($symbol)
354
    {
355
        $locations_by_files = $this->codebase->findReferencesToSymbol($symbol);
356
357
        foreach ($locations_by_files as $locations) {
358
            $bounds_starts = [];
359
360
            foreach ($locations as $location) {
361
                $snippet = $location->getSnippet();
362
363
                $snippet_bounds = $location->getSnippetBounds();
364
                $selection_bounds = $location->getSelectionBounds();
365
366
                if (isset($bounds_starts[$selection_bounds[0]])) {
367
                    continue;
368
                }
369
370
                $bounds_starts[$selection_bounds[0]] = true;
371
372
                $selection_start = $selection_bounds[0] - $snippet_bounds[0];
373
                $selection_length = $selection_bounds[1] - $selection_bounds[0];
374
375
                echo $location->file_name . ':' . $location->getLineNumber() . "\n" .
376
                    (
377
                        $this->use_color
378
                        ? substr($snippet, 0, $selection_start) .
379
                        "\e[97;42m" . substr($snippet, $selection_start, $selection_length) .
380
                        "\e[0m" . substr($snippet, $selection_length + $selection_start)
381
                        : $snippet
382
                    ) . "\n" . "\n";
383
            }
384
        }
385
    }
386
387
    /**
388
     * @param  string  $dir_name
389
     *
390
     * @return void
391
     */
392
    public function checkDir($dir_name)
393
    {
394
        FileReferenceProvider::loadReferenceCache();
395
396
        $this->checkDirWithConfig($dir_name, $this->config, true);
397
398
        if ($this->output_format === self::TYPE_CONSOLE) {
399
            echo 'Scanning files...' . "\n";
400
        }
401
402
        $this->config->initializePlugins($this);
403
404
        $this->codebase->scanFiles();
405
406
        $this->config->visitStubFiles($this->codebase, $this->debug_output);
407
408
        if ($this->output_format === self::TYPE_CONSOLE) {
409
            echo 'Analyzing files...' . "\n";
410
        }
411
412
        $this->codebase->analyzer->analyzeFiles($this, $this->threads, $this->alter_code);
413
    }
414
415
    /**
416
     * @param  string $dir_name
417
     * @param  Config $config
418
     * @param  bool   $allow_non_project_files
419
     *
420
     * @return void
421
     */
422
    private function checkDirWithConfig($dir_name, Config $config, $allow_non_project_files = false)
423
    {
424
        $file_extensions = $config->getFileExtensions();
425
426
        /** @var RecursiveDirectoryIterator */
427
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_name));
428
        $iterator->rewind();
429
430
        $files_to_scan = [];
431
432
        while ($iterator->valid()) {
433
            if (!$iterator->isDot()) {
0 ignored issues
show
Bug introduced by
The method isDot() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

433
            if (!$iterator->/** @scrutinizer ignore-call */ isDot()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
434
                $extension = $iterator->getExtension();
0 ignored issues
show
Bug introduced by
The method getExtension() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

434
                /** @scrutinizer ignore-call */ 
435
                $extension = $iterator->getExtension();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
435
                if (in_array($extension, $file_extensions, true)) {
436
                    $file_path = (string)$iterator->getRealPath();
0 ignored issues
show
Bug introduced by
The method getRealPath() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

436
                    $file_path = (string)$iterator->/** @scrutinizer ignore-call */ getRealPath();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
437
438
                    if ($allow_non_project_files || $config->isInProjectDirs($file_path)) {
439
                        $files_to_scan[$file_path] = $file_path;
440
                    }
441
                }
442
            }
443
444
            $iterator->next();
445
        }
446
447
        $this->codebase->addFilesToAnalyze($files_to_scan);
448
    }
449
450
    /**
451
     * @param  Config $config
452
     *
453
     * @return array<int, string>
454
     */
455
    private function getAllFiles(Config $config)
456
    {
457
        $file_extensions = $config->getFileExtensions();
458
        $file_names = [];
459
460
        foreach ($config->getProjectDirectories() as $dir_name) {
461
            /** @var RecursiveDirectoryIterator */
462
            $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_name));
463
            $iterator->rewind();
464
465
            while ($iterator->valid()) {
466
                if (!$iterator->isDot()) {
467
                    $extension = $iterator->getExtension();
468
                    if (in_array($extension, $file_extensions, true)) {
469
                        $file_names[] = (string)$iterator->getRealPath();
470
                    }
471
                }
472
473
                $iterator->next();
474
            }
475
        }
476
477
        return $file_names;
478
    }
479
480
    /**
481
     * @param  string $dir_name
482
     * @param  Config $config
483
     *
484
     * @return array<string>
485
     */
486
    protected function getDiffFilesInDir($dir_name, Config $config)
487
    {
488
        $file_extensions = $config->getFileExtensions();
489
490
        /** @var RecursiveDirectoryIterator */
491
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_name));
492
        $iterator->rewind();
493
494
        $diff_files = [];
495
496
        while ($iterator->valid()) {
497
            if (!$iterator->isDot()) {
498
                $extension = $iterator->getExtension();
499
                if (in_array($extension, $file_extensions, true)) {
500
                    $file_path = (string)$iterator->getRealPath();
501
502
                    if ($config->isInProjectDirs($file_path)) {
503
                        if ($this->file_provider->getModifiedTime($file_path) > $this->cache_provider->getLastGoodRun()
504
                        ) {
505
                            $diff_files[] = $file_path;
506
                        }
507
                    }
508
                }
509
            }
510
511
            $iterator->next();
512
        }
513
514
        return $diff_files;
515
    }
516
517
    /**
518
     * @param  Config           $config
519
     * @param  array<string>    $file_list
520
     *
521
     * @return void
522
     */
523
    private function checkDiffFilesWithConfig(Config $config, array $file_list = [])
524
    {
525
        $files_to_scan = [];
526
527
        foreach ($file_list as $file_path) {
528
            if (!file_exists($file_path)) {
529
                continue;
530
            }
531
532
            if (!$config->isInProjectDirs($file_path)) {
533
                if ($this->debug_output) {
534
                    echo 'skipping ' . $file_path . "\n";
535
                }
536
537
                continue;
538
            }
539
540
            $files_to_scan[$file_path] = $file_path;
541
        }
542
543
        $this->codebase->addFilesToAnalyze($files_to_scan);
544
    }
545
546
    /**
547
     * @param  string  $file_path
548
     *
549
     * @return void
550
     */
551
    public function checkFile($file_path)
552
    {
553
        if ($this->debug_output) {
554
            echo 'Checking ' . $file_path . "\n";
555
        }
556
557
        $this->config->hide_external_errors = $this->config->isInProjectDirs($file_path);
558
559
        $this->codebase->addFilesToAnalyze([$file_path => $file_path]);
560
561
        FileReferenceProvider::loadReferenceCache();
562
563
        if ($this->output_format === self::TYPE_CONSOLE) {
564
            echo 'Scanning files...' . "\n";
565
        }
566
567
        $this->config->initializePlugins($this);
568
569
        $this->codebase->scanFiles();
570
571
        $this->config->visitStubFiles($this->codebase, $this->debug_output);
572
573
        if ($this->output_format === self::TYPE_CONSOLE) {
574
            echo 'Analyzing files...' . "\n";
575
        }
576
577
        $this->codebase->analyzer->analyzeFiles($this, $this->threads, $this->alter_code);
578
    }
579
580
    /**
581
     * @return Config
582
     */
583
    public function getConfig()
584
    {
585
        return $this->config;
586
    }
587
588
    /**
589
     * @param  array<string>  $diff_files
590
     *
591
     * @return array<string>
592
     */
593
    public static function getReferencedFilesFromDiff(array $diff_files)
594
    {
595
        $all_inherited_files_to_check = $diff_files;
596
597
        while ($diff_files) {
598
            $diff_file = array_shift($diff_files);
599
600
            $dependent_files = FileReferenceProvider::getFilesInheritingFromFile($diff_file);
601
            $new_dependent_files = array_diff($dependent_files, $all_inherited_files_to_check);
602
603
            $all_inherited_files_to_check += $new_dependent_files;
604
            $diff_files += $new_dependent_files;
605
        }
606
607
        $all_files_to_check = $all_inherited_files_to_check;
608
609
        foreach ($all_inherited_files_to_check as $file_name) {
610
            $dependent_files = FileReferenceProvider::getFilesReferencingFile($file_name);
611
            $all_files_to_check = array_merge($dependent_files, $all_files_to_check);
612
        }
613
614
        return array_unique($all_files_to_check);
615
    }
616
617
    /**
618
     * @param  string $file_path
619
     *
620
     * @return bool
621
     */
622
    public function fileExists($file_path)
623
    {
624
        return $this->file_provider->fileExists($file_path);
625
    }
626
627
    /**
628
     * @param int $php_major_version
629
     * @param int $php_minor_version
630
     * @param bool $dry_run
631
     * @param bool $safe_types
632
     *
633
     * @return void
634
     */
635
    public function alterCodeAfterCompletion(
636
        $php_major_version,
637
        $php_minor_version,
638
        $dry_run = false,
639
        $safe_types = false
640
    ) {
641
        $this->alter_code = true;
642
        $this->show_issues = false;
643
        $this->php_major_version = $php_major_version;
644
        $this->php_minor_version = $php_minor_version;
645
        $this->dry_run = $dry_run;
646
        $this->only_replace_php_types_with_non_docblock_types = $safe_types;
647
    }
648
649
    /**
650
     * @param array<string, bool> $issues
651
     *
652
     * @return void
653
     */
654
    public function setIssuesToFix(array $issues)
655
    {
656
        $this->issues_to_fix = $issues;
657
    }
658
659
    /**
660
     * @return array<string, bool>
661
     *
662
     * @psalm-suppress PossiblyUnusedMethod - need to fix #422
663
     */
664
    public function getIssuesToFix()
665
    {
666
        return $this->issues_to_fix;
667
    }
668
669
    /**
670
     * @return Codebase
671
     */
672
    public function getCodebase()
673
    {
674
        return $this->codebase;
675
    }
676
677
    /**
678
     * @param  string $fq_class_name
679
     *
680
     * @return FileChecker
681
     */
682
    public function getFileCheckerForClassLike($fq_class_name)
683
    {
684
        $fq_class_name_lc = strtolower($fq_class_name);
685
686
        $file_path = $this->codebase->scanner->getClassLikeFilePath($fq_class_name_lc);
687
688
        if ($this->cache && isset($this->file_checkers[$file_path])) {
689
            return $this->file_checkers[$file_path];
690
        }
691
692
        $file_checker = new FileChecker(
693
            $this,
694
            $file_path,
695
            $this->config->shortenFileName($file_path)
696
        );
697
698
        if ($this->cache) {
699
            $this->file_checkers[$file_path] = $file_checker;
700
        }
701
702
        return $file_checker;
703
    }
704
705
    /**
706
     * @param  string   $original_method_id
707
     * @param  Context  $this_context
708
     *
709
     * @return void
710
     */
711
    public function getMethodMutations($original_method_id, Context $this_context)
712
    {
713
        list($fq_class_name) = explode('::', $original_method_id);
714
715
        $file_checker = $this->getFileCheckerForClassLike($fq_class_name);
716
717
        $appearing_method_id = $this->codebase->methods->getAppearingMethodId($original_method_id);
718
719
        if (!$appearing_method_id) {
720
            // this can happen for some abstract classes implementing (but not fully) interfaces
721
            return;
722
        }
723
724
        list($appearing_fq_class_name) = explode('::', $appearing_method_id);
725
726
        $appearing_class_storage = $this->classlike_storage_provider->get($appearing_fq_class_name);
727
728
        if (!$appearing_class_storage->user_defined) {
729
            return;
730
        }
731
732
        if (strtolower($appearing_fq_class_name) !== strtolower($fq_class_name)) {
733
            $file_checker = $this->getFileCheckerForClassLike($appearing_fq_class_name);
734
        }
735
736
        $stmts = $this->codebase->getStatementsForFile($file_checker->getFilePath());
737
738
        $file_checker->populateCheckers($stmts);
739
740
        if (!$this_context->self) {
741
            $this_context->self = $fq_class_name;
742
            $this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name);
743
        }
744
745
        $file_checker->getMethodMutations($appearing_method_id, $this_context);
746
    }
747
748
    /**
749
     * @return void
750
     */
751
    public function enableCheckerCache()
752
    {
753
        $this->cache = true;
754
    }
755
756
    /**
757
     * @return void
758
     */
759
    public function disableCheckerCache()
760
    {
761
        $this->cache = false;
762
    }
763
764
    /**
765
     * @return bool
766
     */
767
    public function canCacheCheckers()
768
    {
769
        return $this->cache;
770
    }
771
}
772