ProjectAnalyzer::getDiffFilesInDir()   B
last analyzed

Complexity

Conditions 7
Paths 5

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 5
nop 2
dl 0
loc 27
rs 8.5546
c 0
b 0
f 0
1
<?php
2
namespace Psalm\Internal\Analyzer;
3
4
use Psalm\Codebase;
5
use Psalm\Config;
6
use Psalm\Context;
7
use Psalm\Exception\UnsupportedIssueToFixException;
8
use Psalm\FileManipulation;
9
use Psalm\Internal\LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
10
use Psalm\Internal\Provider\ClassLikeStorageProvider;
11
use Psalm\Internal\Provider\FileProvider;
12
use Psalm\Internal\Provider\FileReferenceProvider;
13
use Psalm\Internal\Provider\ParserCacheProvider;
14
use Psalm\Internal\Provider\ProjectCacheProvider;
15
use Psalm\Internal\Provider\Providers;
16
use Psalm\Issue\InvalidFalsableReturnType;
17
use Psalm\Issue\InvalidNullableReturnType;
18
use Psalm\Issue\InvalidReturnType;
19
use Psalm\Issue\LessSpecificReturnType;
20
use Psalm\Issue\MismatchingDocblockParamType;
21
use Psalm\Issue\MismatchingDocblockReturnType;
22
use Psalm\Issue\MissingClosureReturnType;
23
use Psalm\Issue\MissingParamType;
24
use Psalm\Issue\MissingPropertyType;
25
use Psalm\Issue\MissingReturnType;
26
use Psalm\Issue\PossiblyUndefinedGlobalVariable;
27
use Psalm\Issue\PossiblyUndefinedVariable;
28
use Psalm\Issue\PossiblyUnusedMethod;
29
use Psalm\Issue\PossiblyUnusedProperty;
30
use Psalm\Issue\UnnecessaryVarAnnotation;
31
use Psalm\Issue\UnusedMethod;
32
use Psalm\Issue\UnusedProperty;
33
use Psalm\Issue\UnusedVariable;
34
use Psalm\Progress\Progress;
35
use Psalm\Progress\VoidProgress;
36
use Psalm\Report;
37
use Psalm\Report\ReportOptions;
38
use Psalm\Type;
39
use Psalm\Issue\CodeIssue;
40
use function substr;
41
use function strlen;
42
use function cli_set_process_title;
43
use function stream_socket_client;
44
use function fwrite;
45
use const STDERR;
46
use function stream_set_blocking;
47
use function stream_socket_server;
48
use const STDOUT;
49
use function extension_loaded;
50
use function stream_socket_accept;
51
use function pcntl_fork;
52
use const STDIN;
53
use function microtime;
54
use function array_merge;
55
use function count;
56
use function implode;
57
use function array_diff;
58
use function strpos;
59
use function explode;
60
use function array_pop;
61
use function strtolower;
62
use function usort;
63
use function file_exists;
64
use function dirname;
65
use function mkdir;
66
use function rename;
67
use function is_dir;
68
use function is_file;
69
use const PHP_EOL;
70
use function array_shift;
71
use function array_combine;
72
use function preg_match;
73
use function array_keys;
74
use function array_fill_keys;
75
use function defined;
76
use function trim;
77
use function shell_exec;
78
use function is_string;
79
use function filter_var;
80
use const FILTER_VALIDATE_INT;
81
use function is_int;
82
use function is_readable;
83
use function file_get_contents;
84
use function substr_count;
85
use function array_map;
86
use function end;
87
use Psalm\Internal\Codebase\Taint;
88
89
/**
90
 * @internal
91
 */
92
class ProjectAnalyzer
93
{
94
    /**
95
     * Cached config
96
     *
97
     * @var Config
98
     */
99
    private $config;
100
101
    /**
102
     * @var self
103
     */
104
    public static $instance;
105
106
    /**
107
     * An object representing everything we know about the code
108
     *
109
     * @var Codebase
110
     */
111
    private $codebase;
112
113
    /** @var FileProvider */
114
    private $file_provider;
115
116
    /** @var ClassLikeStorageProvider */
117
    private $classlike_storage_provider;
118
119
    /** @var ?ParserCacheProvider */
120
    private $parser_cache_provider;
121
122
    /** @var ?ProjectCacheProvider */
123
    public $project_cache_provider;
124
125
    /** @var FileReferenceProvider */
126
    private $file_reference_provider;
127
128
    /**
129
     * @var Progress
130
     */
131
    public $progress;
132
133
    /**
134
     * @var bool
135
     */
136
    public $debug_lines = false;
137
138
    /**
139
     * @var bool
140
     */
141
    public $show_issues = true;
142
143
    /** @var int */
144
    public $threads;
145
146
    /**
147
     * @var array<string, bool>
148
     */
149
    private $issues_to_fix = [];
150
151
    /**
152
     * @var bool
153
     */
154
    public $dry_run = false;
155
156
    /**
157
     * @var bool
158
     */
159
    public $full_run = false;
160
161
    /**
162
     * @var bool
163
     */
164
    public $only_replace_php_types_with_non_docblock_types = false;
165
166
    /**
167
     * @var ?int
168
     */
169
    public $onchange_line_limit;
170
171
    /**
172
     * @var bool
173
     */
174
    public $provide_completion = false;
175
176
    /**
177
     * @var array<string,string>
178
     */
179
    private $project_files = [];
180
181
    /**
182
     * @var array<string,string>
183
     */
184
    private $extra_files = [];
185
186
    /**
187
     * @var array<string, string>
188
     */
189
    private $to_refactor = [];
190
191
    /**
192
     * @var ?ReportOptions
193
     */
194
    public $stdout_report_options;
195
196
    /**
197
     * @var array<ReportOptions>
198
     */
199
    public $generated_report_options;
200
201
    /**
202
     * @var array<int, class-string<CodeIssue>>
203
     */
204
    const SUPPORTED_ISSUES_TO_FIX = [
205
        InvalidFalsableReturnType::class,
206
        InvalidNullableReturnType::class,
207
        InvalidReturnType::class,
208
        LessSpecificReturnType::class,
209
        MismatchingDocblockParamType::class,
210
        MismatchingDocblockReturnType::class,
211
        MissingClosureReturnType::class,
212
        MissingParamType::class,
213
        MissingPropertyType::class,
214
        MissingReturnType::class,
215
        PossiblyUndefinedGlobalVariable::class,
216
        PossiblyUndefinedVariable::class,
217
        PossiblyUnusedMethod::class,
218
        PossiblyUnusedProperty::class,
219
        UnusedMethod::class,
220
        UnusedProperty::class,
221
        UnusedVariable::class,
222
        UnnecessaryVarAnnotation::class,
223
    ];
224
225
    /**
226
     * When this is true, the language server will send the diagnostic code with a help link.
227
     *
228
     * @var bool
229
     */
230
    public $language_server_use_extended_diagnostic_codes = false;
231
232
    /**
233
     * If this is true then the language server will send log messages to the client with additional information.
234
     *
235
     * @var bool
236
     */
237
    public $language_server_verbose = false;
238
239
    /**
240
     * @param array<ReportOptions> $generated_report_options
241
     * @param int           $threads
242
     * @param string        $reports
0 ignored issues
show
Bug introduced by
There is no parameter named $reports. 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...
243
     */
244
    public function __construct(
245
        Config $config,
246
        Providers $providers,
247
        ?ReportOptions $stdout_report_options = null,
248
        array $generated_report_options = [],
249
        int $threads = 1,
250
        Progress $progress = null
251
    ) {
252
        if ($progress === null) {
253
            $progress = new VoidProgress();
254
        }
255
256
        $this->parser_cache_provider = $providers->parser_cache_provider;
257
        $this->project_cache_provider = $providers->project_cache_provider;
258
        $this->file_provider = $providers->file_provider;
259
        $this->classlike_storage_provider = $providers->classlike_storage_provider;
260
        $this->file_reference_provider = $providers->file_reference_provider;
261
262
        $this->progress = $progress;
263
        $this->threads = $threads;
264
        $this->config = $config;
265
266
        $this->clearCacheDirectoryIfConfigOrComposerLockfileChanged();
267
268
        $this->codebase = new Codebase(
269
            $config,
270
            $providers,
271
            $progress
272
        );
273
274
        $this->stdout_report_options = $stdout_report_options;
275
        $this->generated_report_options = $generated_report_options;
276
277
        $file_extensions = $this->config->getFileExtensions();
278
279
        foreach ($this->config->getProjectDirectories() as $dir_name) {
280
            $file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
281
282
            foreach ($file_paths as $file_path) {
283
                if ($this->config->isInProjectDirs($file_path)) {
284
                    $this->addProjectFile($file_path);
285
                }
286
            }
287
        }
288
289
        foreach ($this->config->getExtraDirectories() as $dir_name) {
290
            $file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
291
292
            foreach ($file_paths as $file_path) {
293
                if ($this->config->isInExtraDirs($file_path)) {
294
                    $this->addExtraFile($file_path);
295
                }
296
            }
297
        }
298
299
        foreach ($this->config->getProjectFiles() as $file_path) {
300
            $this->addProjectFile($file_path);
301
        }
302
303
        self::$instance = $this;
304
    }
305
306
    private function clearCacheDirectoryIfConfigOrComposerLockfileChanged() : void
307
    {
308
        if ($this->project_cache_provider
309
            && $this->project_cache_provider->hasLockfileChanged()
310
        ) {
311
            $this->progress->debug(
312
                'Composer lockfile change detected, clearing cache' . "\n"
313
            );
314
315
            Config::removeCacheDirectory($this->config->getCacheDirectory());
316
317
            if ($this->file_reference_provider->cache) {
318
                $this->file_reference_provider->cache->hasConfigChanged();
319
            }
320
321
            $this->project_cache_provider->updateComposerLockHash();
322
        } elseif ($this->file_reference_provider->cache
323
            && $this->file_reference_provider->cache->hasConfigChanged()
324
        ) {
325
            $this->progress->debug(
326
                'Config change detected, clearing cache' . "\n"
327
            );
328
329
            Config::removeCacheDirectory($this->config->getCacheDirectory());
330
331
            if ($this->project_cache_provider) {
332
                $this->project_cache_provider->hasLockfileChanged();
333
            }
334
        }
335
    }
336
337
    /**
338
     * @param  array<string>  $report_file_paths
339
     * @param  bool   $show_info
340
     * @return array<ReportOptions>
341
     */
342
    public static function getFileReportOptions(array $report_file_paths, bool $show_info = true)
343
    {
344
        $report_options = [];
345
346
        $mapping = [
347
            'checkstyle.xml' => Report::TYPE_CHECKSTYLE,
348
            'sonarqube.json' => Report::TYPE_SONARQUBE,
349
            'summary.json' => Report::TYPE_JSON_SUMMARY,
350
            'junit.xml' => Report::TYPE_JUNIT,
351
            '.xml' => Report::TYPE_XML,
352
            '.json' => Report::TYPE_JSON,
353
            '.txt' => Report::TYPE_TEXT,
354
            '.emacs' => Report::TYPE_EMACS,
355
            '.pylint' => Report::TYPE_PYLINT,
356
            '.console' => Report::TYPE_CONSOLE,
357
        ];
358
359
        foreach ($report_file_paths as $report_file_path) {
360
            foreach ($mapping as $extension => $type) {
361
                if (substr($report_file_path, -strlen($extension)) === $extension) {
362
                    $o = new ReportOptions();
363
364
                    $o->format = $type;
365
                    $o->show_info = $show_info;
366
                    $o->output_path = $report_file_path;
367
                    $o->use_color = false;
368
                    $report_options[] = $o;
369
                    continue 2;
370
                }
371
            }
372
373
            throw new \UnexpectedValueException('Unknown report format ' . $report_file_path);
374
        }
375
376
        return $report_options;
377
    }
378
379
    private function visitAutoloadFiles() : void
380
    {
381
        $start_time = microtime(true);
382
383
        $this->config->visitComposerAutoloadFiles($this, $this->progress);
384
385
        $now_time = microtime(true);
386
387
        $this->progress->debug(
388
            'Visiting autoload files took ' . \number_format($now_time - $start_time, 3) . 's' . "\n"
389
        );
390
    }
391
392
    /**
393
     * @param  string|null $address
394
     * @return void
395
     */
396
    public function server($address = '127.0.0.1:12345', bool $socket_server_mode = false)
397
    {
398
        $this->visitAutoloadFiles();
399
        $this->codebase->diff_methods = true;
400
        $this->file_reference_provider->loadReferenceCache();
401
        $this->codebase->enterServerMode();
402
403
        if (\ini_get('pcre.jit') === '1'
404
            && \PHP_OS === 'Darwin'
405
            && \version_compare(\PHP_VERSION, '7.3.0') >= 0
406
            && \version_compare(\PHP_VERSION, '7.4.0') < 0
407
        ) {
408
            // do nothing
409
        } else {
410
            $cpu_count = self::getCpuCount();
411
412
            // let's not go crazy
413
            $usable_cpus = $cpu_count - 2;
414
415
            if ($usable_cpus > 1) {
416
                $this->threads = $usable_cpus;
417
            }
418
        }
419
420
        $this->config->initializePlugins($this);
421
422
        foreach ($this->config->getProjectDirectories() as $dir_name) {
423
            $this->checkDirWithConfig($dir_name, $this->config);
424
        }
425
426
        @cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
427
428
        if (!$socket_server_mode && $address) {
429
            // Connect to a TCP server
430
            $socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
431
            if ($socket === false) {
432
                fwrite(STDERR, "Could not connect to language client. Error $errno\n$errstr");
433
                exit(1);
434
            }
435
            stream_set_blocking($socket, false);
436
            new LanguageServer(
437
                new ProtocolStreamReader($socket),
438
                new ProtocolStreamWriter($socket),
439
                $this
440
            );
441
            \Amp\Loop::run();
442
        } elseif ($socket_server_mode && $address) {
443
            // Run a TCP Server
444
            $tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
445
            if ($tcpServer === false) {
446
                fwrite(STDERR, "Could not listen on $address. Error $errno\n$errstr");
447
                exit(1);
448
            }
449
            fwrite(STDOUT, "Server listening on $address\n");
450
            if (!extension_loaded('pcntl')) {
451
                fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n");
452
            }
453
            while ($socket = stream_socket_accept($tcpServer, -1)) {
454
                fwrite(STDOUT, "Connection accepted\n");
455
                stream_set_blocking($socket, false);
456
                if (extension_loaded('pcntl')) {
457
                    // If PCNTL is available, fork a child process for the connection
458
                    // An exit notification will only terminate the child process
459
                    $pid = pcntl_fork();
460
                    if ($pid === -1) {
461
                        fwrite(STDERR, "Could not fork\n");
462
                        exit(1);
463
                    }
464
465
                    if ($pid === 0) {
466
                        // Child process
467
                        $reader = new ProtocolStreamReader($socket);
468
                        $reader->on(
469
                            'close',
470
                            /** @return void */
471
                            function () {
472
                                fwrite(STDOUT, "Connection closed\n");
473
                            }
474
                        );
475
                        new LanguageServer(
476
                            $reader,
477
                            new ProtocolStreamWriter($socket),
478
                            $this
479
                        );
480
                        // Just for safety
481
                        exit(0);
482
                    }
483
                } else {
484
                    // If PCNTL is not available, we only accept one connection.
485
                    // An exit notification will terminate the server
486
                    new LanguageServer(
487
                        new ProtocolStreamReader($socket),
488
                        new ProtocolStreamWriter($socket),
489
                        $this
490
                    );
491
                    \Amp\Loop::run();
492
                }
493
            }
494
        } else {
495
            // Use STDIO
496
            stream_set_blocking(STDIN, false);
497
            new LanguageServer(
498
                new ProtocolStreamReader(STDIN),
0 ignored issues
show
Documentation introduced by
\STDIN is of type string, but the function expects a resource.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
499
                new ProtocolStreamWriter(STDOUT),
0 ignored issues
show
Documentation introduced by
\STDOUT is of type string, but the function expects a resource.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
500
                $this
501
            );
502
            \Amp\Loop::run();
503
        }
504
    }
505
506
    /**
507
     * @return self
508
     */
509
    public static function getInstance()
510
    {
511
        return self::$instance;
512
    }
513
514
    /**
515
     * @param  string $file_path
516
     *
517
     * @return bool
518
     */
519
    public function canReportIssues($file_path)
520
    {
521
        return isset($this->project_files[$file_path]);
522
    }
523
524
    /**
525
     * @param  string  $base_dir
526
     * @param  bool $is_diff
527
     *
528
     * @return void
529
     */
530
    public function check($base_dir, $is_diff = false)
531
    {
532
        $start_checks = (int)microtime(true);
533
534
        if (!$base_dir) {
535
            throw new \InvalidArgumentException('Cannot work with empty base_dir');
536
        }
537
538
        $diff_files = null;
539
        $deleted_files = null;
540
541
        $this->full_run = true;
542
543
        $reference_cache = $this->file_reference_provider->loadReferenceCache(true);
544
545
        $this->codebase->diff_methods = $is_diff;
546
547
        if ($is_diff
548
            && $reference_cache
549
            && $this->project_cache_provider
550
            && $this->project_cache_provider->canDiffFiles()
551
        ) {
552
            $deleted_files = $this->file_reference_provider->getDeletedReferencedFiles();
553
            $diff_files = $deleted_files;
554
555
            foreach ($this->config->getProjectDirectories() as $dir_name) {
556
                $diff_files = array_merge($diff_files, $this->getDiffFilesInDir($dir_name, $this->config));
557
            }
558
        }
559
560
        $this->progress->startScanningFiles();
561
562
        $diff_no_files = false;
563
564
        if ($diff_files === null
565
            || $deleted_files === null
566
            || count($diff_files) > 200
567
        ) {
568
            $this->visitAutoloadFiles();
569
570
            $this->codebase->scanner->addFilesToShallowScan($this->extra_files);
571
            $this->codebase->scanner->addFilesToDeepScan($this->project_files);
572
            $this->codebase->analyzer->addFilesToAnalyze($this->project_files);
573
574
            $this->config->initializePlugins($this);
575
576
            $this->codebase->scanFiles($this->threads);
577
578
            $this->codebase->infer_types_from_usage = true;
579
        } else {
580
            $this->progress->debug(count($diff_files) . ' changed files: ' . "\n");
581
            $this->progress->debug('    ' . implode("\n    ", $diff_files) . "\n");
582
583
            $this->codebase->analyzer->addFilesToShowResults($this->project_files);
584
585
            if ($diff_files) {
586
                $file_list = $this->getReferencedFilesFromDiff($diff_files);
587
588
                // strip out deleted files
589
                $file_list = array_diff($file_list, $deleted_files);
590
591
                if ($file_list) {
592
                    $this->visitAutoloadFiles();
593
594
                    $this->checkDiffFilesWithConfig($this->config, $file_list);
595
596
                    $this->config->initializePlugins($this);
597
598
                    $this->codebase->scanFiles($this->threads);
599
                } else {
600
                    $diff_no_files = true;
601
                }
602
            } else {
603
                $diff_no_files = true;
604
            }
605
        }
606
607
        if (!$diff_no_files) {
608
            $this->config->visitStubFiles($this->codebase, $this->progress);
609
610
            $plugin_classes = $this->config->after_codebase_populated;
611
612
            if ($plugin_classes) {
613
                foreach ($plugin_classes as $plugin_fq_class_name) {
614
                    $plugin_fq_class_name::afterCodebasePopulated($this->codebase);
615
                }
616
            }
617
        }
618
619
        $this->progress->startAnalyzingFiles();
620
621
        $this->codebase->analyzer->analyzeFiles(
622
            $this,
623
            $this->threads,
624
            $this->codebase->alter_code,
625
            true
626
        );
627
628
        if ($this->project_cache_provider && $this->parser_cache_provider) {
629
            $removed_parser_files = $this->parser_cache_provider->deleteOldParserCaches(
630
                $is_diff ? $this->project_cache_provider->getLastRun() : $start_checks
631
            );
632
633
            if ($removed_parser_files) {
634
                $this->progress->debug('Removed ' . $removed_parser_files . ' old parser caches' . "\n");
635
            }
636
637
            if ($is_diff) {
638
                $this->parser_cache_provider->touchParserCaches($this->getAllFiles($this->config), $start_checks);
639
            }
640
        }
641
    }
642
643
    /**
644
     * @return void
645
     */
646
    public function consolidateAnalyzedData()
647
    {
648
        $this->codebase->classlikes->consolidateAnalyzedData(
649
            $this->codebase->methods,
650
            $this->progress,
651
            !!$this->codebase->find_unused_code
652
        );
653
    }
654
655
    /**
656
     * @return void
657
     */
658
    public function trackTaintedInputs()
659
    {
660
        $this->codebase->taint = new Taint();
661
    }
662
663
    /**
664
     * @return void
665
     */
666
    public function trackUnusedSuppressions()
667
    {
668
        $this->codebase->track_unused_suppressions = true;
669
    }
670
671
    public function interpretRefactors() : void
672
    {
673
        if (!$this->codebase->alter_code) {
674
            throw new \UnexpectedValueException('Should not be checking references');
675
        }
676
677
        // interpret wildcards
678
        foreach ($this->to_refactor as $source => $destination) {
679
            if (($source_pos = strpos($source, '*'))
680
                && ($destination_pos = strpos($destination, '*'))
681
                && $source_pos === (strlen($source) - 1)
682
                && $destination_pos === (strlen($destination) - 1)
683
            ) {
684
                foreach ($this->codebase->classlike_storage_provider->getAll() as $class_storage) {
685
                    if (substr($class_storage->name, 0, $source_pos) === substr($source, 0, -1)) {
686
                        $this->to_refactor[$class_storage->name]
687
                            = substr($destination, 0, -1) . substr($class_storage->name, $source_pos);
688
                    }
689
                }
690
691
                unset($this->to_refactor[$source]);
692
            }
693
        }
694
695
        foreach ($this->to_refactor as $source => $destination) {
696
            $source_parts = explode('::', $source);
697
            $destination_parts = explode('::', $destination);
698
699
            if (!$this->codebase->classlikes->hasFullyQualifiedClassName($source_parts[0])) {
700
                throw new \Psalm\Exception\RefactorException(
701
                    'Source class ' . $source_parts[0] . ' doesn’t exist'
702
                );
703
            }
704
705
            if (count($source_parts) === 1 && count($destination_parts) === 1) {
706
                if ($this->codebase->classlikes->hasFullyQualifiedClassName($destination_parts[0])) {
707
                    throw new \Psalm\Exception\RefactorException(
708
                        'Destination class ' . $destination_parts[0] . ' already exists'
709
                    );
710
                }
711
712
                $source_class_storage = $this->codebase->classlike_storage_provider->get($source_parts[0]);
713
714
                $destination_parts = explode('\\', $destination);
715
716
                array_pop($destination_parts);
717
                $destination_ns = implode('\\', $destination_parts);
718
719
                $this->codebase->classes_to_move[strtolower($source)] = $destination;
720
721
                $destination_class_storage = $this->codebase->classlike_storage_provider->create($destination);
722
723
                $destination_class_storage->name = $destination;
724
725
                if ($source_class_storage->aliases) {
726
                    $destination_class_storage->aliases = clone $source_class_storage->aliases;
727
                    $destination_class_storage->aliases->namespace = $destination_ns;
728
                }
729
730
                $destination_class_storage->location = $source_class_storage->location;
731
                $destination_class_storage->stmt_location = $source_class_storage->stmt_location;
732
                $destination_class_storage->populated = true;
733
734
                $this->codebase->class_transforms[strtolower($source)] = $destination;
735
736
                continue;
737
            }
738
739
            $source_method_id = new \Psalm\Internal\MethodIdentifier(
740
                $source_parts[0],
741
                strtolower($source_parts[1])
742
            );
743
744
            if ($this->codebase->methods->methodExists($source_method_id)) {
745
                if ($this->codebase->methods->methodExists(
746
                    new \Psalm\Internal\MethodIdentifier(
747
                        $destination_parts[0],
748
                        strtolower($destination_parts[1])
749
                    )
750
                )) {
751
                    throw new \Psalm\Exception\RefactorException(
752
                        'Destination method ' . $destination . ' already exists'
753
                    );
754
                }
755
756
                if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
757
                    throw new \Psalm\Exception\RefactorException(
758
                        'Destination class ' . $destination_parts[0] . ' doesn’t exist'
759
                    );
760
                }
761
762
                if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
763
                    $source_method_storage = $this->codebase->methods->getStorage($source_method_id);
764
                    $destination_class_storage
765
                        = $this->codebase->classlike_storage_provider->get($destination_parts[0]);
766
767
                    if (!$source_method_storage->is_static
768
                        && !isset(
769
                            $destination_class_storage->parent_classes[strtolower($source_method_id->fq_class_name)]
770
                        )
771
                    ) {
772
                        throw new \Psalm\Exception\RefactorException(
773
                            'Cannot move non-static method ' . $source
774
                                . ' into unrelated class ' . $destination_parts[0]
775
                        );
776
                    }
777
778
                    $this->codebase->methods_to_move[strtolower($source)]= $destination;
779
                } else {
780
                    $this->codebase->methods_to_rename[strtolower($source)] = $destination_parts[1];
781
                }
782
783
                $this->codebase->call_transforms[strtolower($source) . '\((.*\))'] = $destination . '($1)';
784
                continue;
785
            }
786
787
            if ($source_parts[1][0] === '$') {
788
                if ($destination_parts[1][0] !== '$') {
789
                    throw new \Psalm\Exception\RefactorException(
790
                        'Destination property must be of the form Foo::$bar'
791
                    );
792
                }
793
794
                if (!$this->codebase->properties->propertyExists($source, true)) {
795
                    throw new \Psalm\Exception\RefactorException(
796
                        'Property ' . $source . ' does not exist'
797
                    );
798
                }
799
800
                if ($this->codebase->properties->propertyExists($destination, true)) {
801
                    throw new \Psalm\Exception\RefactorException(
802
                        'Destination property ' . $destination . ' already exists'
803
                    );
804
                }
805
806
                if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
807
                    throw new \Psalm\Exception\RefactorException(
808
                        'Destination class ' . $destination_parts[0] . ' doesn’t exist'
809
                    );
810
                }
811
812
                $source_id = strtolower($source_parts[0]) . '::' . $source_parts[1];
813
814
                if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
815
                    $source_storage = $this->codebase->properties->getStorage($source);
816
817
                    if (!$source_storage->is_static) {
818
                        throw new \Psalm\Exception\RefactorException(
819
                            'Cannot move non-static property ' . $source
820
                        );
821
                    }
822
823
                    $this->codebase->properties_to_move[$source_id] = $destination;
824
                } else {
825
                    $this->codebase->properties_to_rename[$source_id] = substr($destination_parts[1], 1);
826
                }
827
828
                $this->codebase->property_transforms[$source_id] = $destination;
829
                continue;
830
            }
831
832
            $source_class_constants = $this->codebase->classlikes->getConstantsForClass(
833
                $source_parts[0],
834
                \ReflectionProperty::IS_PRIVATE
835
            );
836
837
            if (isset($source_class_constants[$source_parts[1]])) {
838
                if (!$this->codebase->classlikes->hasFullyQualifiedClassName($destination_parts[0])) {
839
                    throw new \Psalm\Exception\RefactorException(
840
                        'Destination class ' . $destination_parts[0] . ' doesn’t exist'
841
                    );
842
                }
843
844
                $destination_class_constants = $this->codebase->classlikes->getConstantsForClass(
845
                    $destination_parts[0],
846
                    \ReflectionProperty::IS_PRIVATE
847
                );
848
849
                if (isset($destination_class_constants[$destination_parts[1]])) {
850
                    throw new \Psalm\Exception\RefactorException(
851
                        'Destination constant ' . $destination . ' already exists'
852
                    );
853
                }
854
855
                $source_id = strtolower($source_parts[0]) . '::' . $source_parts[1];
856
857
                if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
858
                    $this->codebase->class_constants_to_move[$source_id] = $destination;
859
                } else {
860
                    $this->codebase->class_constants_to_rename[$source_id] = $destination_parts[1];
861
                }
862
863
                $this->codebase->class_constant_transforms[$source_id] = $destination;
864
                continue;
865
            }
866
867
            throw new \Psalm\Exception\RefactorException(
868
                'Psalm cannot locate ' . $source
869
            );
870
        }
871
    }
872
873
    public function prepareMigration() : void
874
    {
875
        if (!$this->codebase->alter_code) {
876
            throw new \UnexpectedValueException('Should not be checking references');
877
        }
878
879
        $this->codebase->classlikes->moveMethods(
880
            $this->codebase->methods,
881
            $this->progress
882
        );
883
884
        $this->codebase->classlikes->moveProperties(
885
            $this->codebase->properties,
886
            $this->progress
887
        );
888
889
        $this->codebase->classlikes->moveClassConstants(
890
            $this->progress
891
        );
892
    }
893
894
    public function migrateCode() : void
895
    {
896
        if (!$this->codebase->alter_code) {
897
            throw new \UnexpectedValueException('Should not be checking references');
898
        }
899
900
        $migration_manipulations = \Psalm\Internal\FileManipulation\FileManipulationBuffer::getMigrationManipulations(
901
            $this->codebase->file_provider
902
        );
903
904
        if ($migration_manipulations) {
905
            foreach ($migration_manipulations as $file_path => $file_manipulations) {
906
                usort(
907
                    $file_manipulations,
908
                    /**
909
                     * @return int
910
                     */
911
                    function (FileManipulation $a, FileManipulation $b) {
912
                        if ($a->start === $b->start) {
913
                            if ($b->end === $a->end) {
914
                                return $b->insertion_text > $a->insertion_text ? 1 : -1;
915
                            }
916
917
                            return $b->end > $a->end ? 1 : -1;
918
                        }
919
920
                        return $b->start > $a->start ? 1 : -1;
921
                    }
922
                );
923
924
                $existing_contents = $this->codebase->file_provider->getContents($file_path);
925
926
                foreach ($file_manipulations as $manipulation) {
927
                    $existing_contents = $manipulation->transform($existing_contents);
928
                }
929
930
                $this->codebase->file_provider->setContents($file_path, $existing_contents);
931
            }
932
        }
933
934
        if ($this->codebase->classes_to_move) {
935
            foreach ($this->codebase->classes_to_move as $source => $destination) {
936
                $source_class_storage = $this->codebase->classlike_storage_provider->get($source);
937
938
                if (!$source_class_storage->location) {
939
                    continue;
940
                }
941
942
                $potential_file_path = $this->config->getPotentialComposerFilePathForClassLike($destination);
943
944
                if ($potential_file_path && !file_exists($potential_file_path)) {
945
                    $containing_dir = dirname($potential_file_path);
946
947
                    if (!file_exists($containing_dir)) {
948
                        mkdir($containing_dir, 0777, true);
949
                    }
950
951
                    rename($source_class_storage->location->file_path, $potential_file_path);
952
                }
953
            }
954
        }
955
    }
956
957
    /**
958
     * @param  string $symbol
959
     *
960
     * @return void
961
     */
962
    public function findReferencesTo($symbol)
963
    {
964
        if (!$this->stdout_report_options) {
965
            throw new \UnexpectedValueException('Not expecting to emit output');
966
        }
967
968
        $locations = $this->codebase->findReferencesToSymbol($symbol);
969
970
        foreach ($locations as $location) {
971
            $snippet = $location->getSnippet();
972
973
            $snippet_bounds = $location->getSnippetBounds();
974
            $selection_bounds = $location->getSelectionBounds();
975
976
            $selection_start = $selection_bounds[0] - $snippet_bounds[0];
977
            $selection_length = $selection_bounds[1] - $selection_bounds[0];
978
979
            echo $location->file_name . ':' . $location->getLineNumber() . "\n" .
980
                (
981
                    $this->stdout_report_options->use_color
982
                    ? substr($snippet, 0, $selection_start) .
983
                    "\e[97;42m" . substr($snippet, $selection_start, $selection_length) .
984
                    "\e[0m" . substr($snippet, $selection_length + $selection_start)
985
                    : $snippet
986
                ) . "\n" . "\n";
987
        }
988
    }
989
990
    /**
991
     * @param  string  $dir_name
992
     *
993
     * @return void
994
     */
995
    public function checkDir($dir_name)
996
    {
997
        $this->file_reference_provider->loadReferenceCache();
998
999
        $this->checkDirWithConfig($dir_name, $this->config, true);
1000
1001
        $this->progress->startScanningFiles();
1002
1003
        $this->config->initializePlugins($this);
1004
1005
        $this->codebase->scanFiles($this->threads);
1006
1007
        $this->config->visitStubFiles($this->codebase, $this->progress);
1008
1009
        $this->progress->startAnalyzingFiles();
1010
1011
        $this->codebase->analyzer->analyzeFiles(
1012
            $this,
1013
            $this->threads,
1014
            $this->codebase->alter_code,
1015
            $this->codebase->find_unused_code === 'always'
1016
        );
1017
    }
1018
1019
    /**
1020
     * @param  string $dir_name
1021
     * @param  Config $config
1022
     * @param  bool   $allow_non_project_files
1023
     *
1024
     * @return void
1025
     */
1026
    private function checkDirWithConfig($dir_name, Config $config, $allow_non_project_files = false)
1027
    {
1028
        $file_extensions = $config->getFileExtensions();
1029
1030
        $file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
1031
1032
        $files_to_scan = [];
1033
1034
        foreach ($file_paths as $file_path) {
1035
            if ($allow_non_project_files || $config->isInProjectDirs($file_path)) {
1036
                $files_to_scan[$file_path] = $file_path;
1037
            }
1038
        }
1039
1040
        $this->codebase->addFilesToAnalyze($files_to_scan);
1041
    }
1042
1043
    /**
1044
     * @param  Config $config
1045
     *
1046
     * @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...
1047
     */
1048
    private function getAllFiles(Config $config)
1049
    {
1050
        $file_extensions = $config->getFileExtensions();
1051
        $file_paths = [];
1052
1053
        foreach ($config->getProjectDirectories() as $dir_name) {
1054
            $file_paths = array_merge(
1055
                $file_paths,
1056
                $this->file_provider->getFilesInDir($dir_name, $file_extensions)
1057
            );
1058
        }
1059
1060
        return $file_paths;
1061
    }
1062
1063
    /**
1064
     * @param  string  $dir_name
0 ignored issues
show
Bug introduced by
There is no parameter named $dir_name. 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...
1065
     *
1066
     * @return void
1067
     */
1068
    public function addProjectFile(string $file_path)
1069
    {
1070
        $this->project_files[$file_path] = $file_path;
1071
    }
1072
1073
    public function addExtraFile(string $file_path) : void
1074
    {
1075
        $this->extra_files[$file_path] = $file_path;
1076
    }
1077
1078
    /**
1079
     * @param  string $dir_name
1080
     * @param  Config $config
1081
     *
1082
     * @return array<string>
1083
     */
1084
    protected function getDiffFilesInDir($dir_name, Config $config)
1085
    {
1086
        $file_extensions = $config->getFileExtensions();
1087
1088
        if (!$this->parser_cache_provider || !$this->project_cache_provider) {
1089
            throw new \UnexpectedValueException('Parser cache provider cannot be null here');
1090
        }
1091
1092
        $diff_files = [];
1093
1094
        $last_run = $this->project_cache_provider->getLastRun();
1095
1096
        $file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
1097
1098
        foreach ($file_paths as $file_path) {
1099
            if ($config->isInProjectDirs($file_path)) {
1100
                if ($this->file_provider->getModifiedTime($file_path) > $last_run
1101
                    && $this->parser_cache_provider->loadExistingFileContentsFromCache($file_path)
1102
                        !== $this->file_provider->getContents($file_path)
1103
                ) {
1104
                    $diff_files[] = $file_path;
1105
                }
1106
            }
1107
        }
1108
1109
        return $diff_files;
1110
    }
1111
1112
    /**
1113
     * @param  Config           $config
1114
     * @param  array<string>    $file_list
1115
     *
1116
     * @return void
1117
     */
1118
    private function checkDiffFilesWithConfig(Config $config, array $file_list = [])
1119
    {
1120
        $files_to_scan = [];
1121
1122
        foreach ($file_list as $file_path) {
1123
            if (!$this->file_provider->fileExists($file_path)) {
1124
                continue;
1125
            }
1126
1127
            if (!$config->isInProjectDirs($file_path)) {
1128
                $this->progress->debug('skipping ' . $file_path . "\n");
1129
1130
                continue;
1131
            }
1132
1133
            $files_to_scan[$file_path] = $file_path;
1134
        }
1135
1136
        $this->codebase->addFilesToAnalyze($files_to_scan);
1137
    }
1138
1139
    /**
1140
     * @param  string  $file_path
1141
     *
1142
     * @return void
1143
     */
1144
    public function checkFile($file_path)
1145
    {
1146
        $this->progress->debug('Checking ' . $file_path . "\n");
1147
1148
        $this->config->hide_external_errors = $this->config->isInProjectDirs($file_path);
1149
1150
        $this->codebase->addFilesToAnalyze([$file_path => $file_path]);
1151
1152
        $this->file_reference_provider->loadReferenceCache();
1153
1154
        $this->progress->startScanningFiles();
1155
1156
        $this->config->initializePlugins($this);
1157
1158
        $this->codebase->scanFiles($this->threads);
1159
1160
        $this->config->visitStubFiles($this->codebase, $this->progress);
1161
1162
        $this->progress->startAnalyzingFiles();
1163
1164
        $this->codebase->analyzer->analyzeFiles(
1165
            $this,
1166
            $this->threads,
1167
            $this->codebase->alter_code,
1168
            $this->codebase->find_unused_code === 'always'
1169
        );
1170
    }
1171
1172
    /**
1173
     * @param string[] $paths_to_check
1174
     * @return void
1175
     */
1176
    public function checkPaths(array $paths_to_check)
1177
    {
1178
        $this->visitAutoloadFiles();
1179
1180
        foreach ($paths_to_check as $path) {
1181
            $this->progress->debug('Checking ' . $path . "\n");
1182
1183
            if (is_dir($path)) {
1184
                $this->checkDirWithConfig($path, $this->config, true);
1185
            } elseif (is_file($path)) {
1186
                $this->codebase->addFilesToAnalyze([$path => $path]);
1187
                $this->config->hide_external_errors = $this->config->isInProjectDirs($path);
1188
            }
1189
        }
1190
1191
        $this->file_reference_provider->loadReferenceCache();
1192
1193
        $this->progress->startScanningFiles();
1194
1195
        $this->config->initializePlugins($this);
1196
1197
        $this->codebase->scanFiles($this->threads);
1198
1199
        $this->config->visitStubFiles($this->codebase, $this->progress);
1200
1201
        $this->progress->startAnalyzingFiles();
1202
1203
        $this->codebase->analyzer->analyzeFiles(
1204
            $this,
1205
            $this->threads,
1206
            $this->codebase->alter_code,
1207
            $this->codebase->find_unused_code === 'always'
1208
        );
1209
1210
        if ($this->stdout_report_options
1211
            && $this->stdout_report_options->format === Report::TYPE_CONSOLE
1212
            && $this->codebase->collect_references
1213
        ) {
1214
            fwrite(
1215
                STDERR,
1216
                PHP_EOL . 'To whom it may concern: Psalm cannot detect unused classes, methods and properties'
1217
                . PHP_EOL . 'when analyzing individual files and folders. Run on the full project to enable'
1218
                . PHP_EOL . 'complete unused code detection.' . PHP_EOL
1219
            );
1220
        }
1221
    }
1222
1223
    /**
1224
     * @return Config
1225
     */
1226
    public function getConfig()
1227
    {
1228
        return $this->config;
1229
    }
1230
1231
    /**
1232
     * @param  array<string>  $diff_files
1233
     *
1234
     * @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...
1235
     */
1236
    public function getReferencedFilesFromDiff(array $diff_files, bool $include_referencing_files = true)
1237
    {
1238
        $all_inherited_files_to_check = $diff_files;
1239
1240
        while ($diff_files) {
1241
            $diff_file = array_shift($diff_files);
1242
1243
            $dependent_files = $this->file_reference_provider->getFilesInheritingFromFile($diff_file);
1244
1245
            $new_dependent_files = array_diff($dependent_files, $all_inherited_files_to_check);
1246
1247
            $all_inherited_files_to_check = array_merge($all_inherited_files_to_check, $new_dependent_files);
1248
            $diff_files = array_merge($diff_files, $new_dependent_files);
1249
        }
1250
1251
        $all_files_to_check = $all_inherited_files_to_check;
1252
1253
        if ($include_referencing_files) {
1254
            foreach ($all_inherited_files_to_check as $file_name) {
1255
                $dependent_files = $this->file_reference_provider->getFilesReferencingFile($file_name);
1256
                $all_files_to_check = array_merge($dependent_files, $all_files_to_check);
1257
            }
1258
        }
1259
1260
        return array_combine($all_files_to_check, $all_files_to_check);
1261
    }
1262
1263
    /**
1264
     * @param  string $file_path
1265
     *
1266
     * @return bool
1267
     */
1268
    public function fileExists($file_path)
1269
    {
1270
        return $this->file_provider->fileExists($file_path);
1271
    }
1272
1273
    /**
1274
     * @param int $php_major_version
0 ignored issues
show
Bug introduced by
There is no parameter named $php_major_version. 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...
1275
     * @param int $php_minor_version
0 ignored issues
show
Bug introduced by
There is no parameter named $php_minor_version. 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...
1276
     * @param bool $dry_run
1277
     * @param bool $safe_types
1278
     *
1279
     * @return void
1280
     */
1281
    public function alterCodeAfterCompletion(
1282
        $dry_run = false,
1283
        $safe_types = false
1284
    ) {
1285
        $this->codebase->alter_code = true;
1286
        $this->codebase->infer_types_from_usage = true;
1287
        $this->show_issues = false;
1288
        $this->dry_run = $dry_run;
1289
        $this->only_replace_php_types_with_non_docblock_types = $safe_types;
1290
    }
1291
1292
    /**
1293
     * @param array<string, string> $to_refactor
1294
     *
1295
     * @return void
1296
     */
1297
    public function refactorCodeAfterCompletion(array $to_refactor)
1298
    {
1299
        $this->to_refactor = $to_refactor;
1300
        $this->codebase->alter_code = true;
1301
        $this->show_issues = false;
1302
    }
1303
1304
    /**
1305
     * @return void
1306
     */
1307
    public function setPhpVersion(string $version)
1308
    {
1309
        if (!preg_match('/^(5\.[456]|7\.[01234]|8\.[0])(\..*)?$/', $version)) {
1310
            throw new \UnexpectedValueException('Expecting a version number in the format x.y');
1311
        }
1312
1313
        list($php_major_version, $php_minor_version) = explode('.', $version);
1314
1315
        $this->codebase->php_major_version = (int) $php_major_version;
1316
        $this->codebase->php_minor_version = (int) $php_minor_version;
1317
    }
1318
1319
    /**
1320
     * @param array<string, bool> $issues
1321
     * @throws UnsupportedIssueToFixException
1322
     *
1323
     * @return void
1324
     */
1325
    public function setIssuesToFix(array $issues)
1326
    {
1327
        $supported_issues_to_fix = static::getSupportedIssuesToFix();
1328
1329
        $unsupportedIssues = array_diff(array_keys($issues), $supported_issues_to_fix);
1330
1331
        if (! empty($unsupportedIssues)) {
1332
            throw new UnsupportedIssueToFixException(
1333
                'Psalm doesn\'t know how to fix issue(s): ' . implode(', ', $unsupportedIssues) . PHP_EOL
1334
                . 'Supported issues to fix are: ' . implode(',', $supported_issues_to_fix)
1335
            );
1336
        }
1337
1338
        $this->issues_to_fix = $issues;
1339
    }
1340
1341
    public function setAllIssuesToFix(): void
1342
    {
1343
        $keyed_issues = array_fill_keys(static::getSupportedIssuesToFix(), true);
1344
1345
        $this->setIssuesToFix($keyed_issues);
1346
    }
1347
1348
    /**
1349
     * @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...
1350
     */
1351
    public function getIssuesToFix()
1352
    {
1353
        return $this->issues_to_fix;
1354
    }
1355
1356
    /**
1357
     * @return Codebase
1358
     */
1359
    public function getCodebase()
1360
    {
1361
        return $this->codebase;
1362
    }
1363
1364
    /**
1365
     * @param  string $fq_class_name
1366
     *
1367
     * @return FileAnalyzer
1368
     */
1369
    public function getFileAnalyzerForClassLike($fq_class_name)
1370
    {
1371
        $fq_class_name_lc = strtolower($fq_class_name);
1372
1373
        $file_path = $this->codebase->scanner->getClassLikeFilePath($fq_class_name_lc);
1374
1375
        $file_analyzer = new FileAnalyzer(
1376
            $this,
1377
            $file_path,
1378
            $this->config->shortenFileName($file_path)
1379
        );
1380
1381
        return $file_analyzer;
1382
    }
1383
1384
    /**
1385
     * @param  Context  $this_context
1386
     *
1387
     * @return void
1388
     */
1389
    public function getMethodMutations(
1390
        \Psalm\Internal\MethodIdentifier $original_method_id,
1391
        Context $this_context,
1392
        string $root_file_path,
1393
        string $root_file_name
1394
    ) {
1395
        $fq_class_name = $original_method_id->fq_class_name;
1396
1397
        $appearing_method_id = $this->codebase->methods->getAppearingMethodId($original_method_id);
1398
1399
        if (!$appearing_method_id) {
1400
            // this can happen for some abstract classes implementing (but not fully) interfaces
1401
            return;
1402
        }
1403
1404
        $appearing_fq_class_name = $appearing_method_id->fq_class_name;
1405
1406
        $appearing_class_storage = $this->classlike_storage_provider->get($appearing_fq_class_name);
1407
1408
        if (!$appearing_class_storage->user_defined) {
1409
            return;
1410
        }
1411
1412
        $file_analyzer = $this->getFileAnalyzerForClassLike($fq_class_name);
1413
1414
        $file_analyzer->setRootFilePath($root_file_path, $root_file_name);
1415
1416
        if ($appearing_fq_class_name !== $fq_class_name) {
1417
            $file_analyzer = $this->getFileAnalyzerForClassLike($appearing_fq_class_name);
1418
        }
1419
1420
        $stmts = $this->codebase->getStatementsForFile(
1421
            $file_analyzer->getFilePath()
1422
        );
1423
1424
        $file_analyzer->populateCheckers($stmts);
1425
1426
        if (!$this_context->self) {
1427
            $this_context->self = $fq_class_name;
1428
            $this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name);
1429
        }
1430
1431
        $file_analyzer->getMethodMutations($appearing_method_id, $this_context, true);
1432
1433
        $file_analyzer->class_analyzers_to_analyze = [];
1434
        $file_analyzer->interface_analyzers_to_analyze = [];
1435
        $file_analyzer->clearSourceBeforeDestruction();
1436
    }
1437
1438
    public function getFunctionLikeAnalyzer(
1439
        \Psalm\Internal\MethodIdentifier $method_id,
1440
        string $file_path
1441
    ) : ?FunctionLikeAnalyzer {
1442
        $file_analyzer = new FileAnalyzer(
1443
            $this,
1444
            $file_path,
1445
            $this->config->shortenFileName($file_path)
1446
        );
1447
1448
        $stmts = $this->codebase->getStatementsForFile(
1449
            $file_analyzer->getFilePath()
1450
        );
1451
1452
        $file_analyzer->populateCheckers($stmts);
1453
1454
        $function_analyzer = $file_analyzer->getFunctionLikeAnalyzer($method_id);
1455
1456
        $file_analyzer->class_analyzers_to_analyze = [];
1457
        $file_analyzer->interface_analyzers_to_analyze = [];
1458
1459
        return $function_analyzer;
1460
    }
1461
1462
    /**
1463
     * Adapted from https://gist.github.com/divinity76/01ef9ca99c111565a72d3a8a6e42f7fb
1464
     * returns number of cpu cores
1465
     * Copyleft 2018, license: WTFPL
1466
     * @throws \RuntimeException
1467
     * @throws \LogicException
1468
     * @return int
1469
     * @psalm-suppress ForbiddenCode
1470
     */
1471
    public static function getCpuCount(): int
1472
    {
1473
        if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
1474
            /*
1475
            $str = trim((string) shell_exec('wmic cpu get NumberOfCores 2>&1'));
1476
            if (!preg_match('/(\d+)/', $str, $matches)) {
1477
                throw new \RuntimeException('wmic failed to get number of cpu cores on windows!');
1478
            }
1479
            return ((int) $matches [1]);
1480
            */
1481
            return 1;
1482
        }
1483
1484
        if (\ini_get('pcre.jit') === '1'
1485
            && \PHP_OS === 'Darwin'
1486
            && \version_compare(\PHP_VERSION, '7.3.0') >= 0
1487
            && \version_compare(\PHP_VERSION, '7.4.0') < 0
1488
        ) {
1489
            return 1;
1490
        }
1491
1492
        if (!extension_loaded('pcntl')) {
1493
            return 1;
1494
        }
1495
1496
        $has_nproc = trim((string) @shell_exec('command -v nproc'));
1497
        if ($has_nproc) {
1498
            $ret = @shell_exec('nproc');
1499
            if (is_string($ret)) {
1500
                $ret = trim($ret);
1501
                $tmp = filter_var($ret, FILTER_VALIDATE_INT);
1502
                if (is_int($tmp)) {
1503
                    return $tmp;
1504
                }
1505
            }
1506
        }
1507
1508
        $ret = @shell_exec('sysctl -n hw.ncpu');
1509
        if (is_string($ret)) {
1510
            $ret = trim($ret);
1511
            $tmp = filter_var($ret, FILTER_VALIDATE_INT);
1512
            if (is_int($tmp)) {
1513
                return $tmp;
1514
            }
1515
        }
1516
1517
        if (is_readable('/proc/cpuinfo')) {
1518
            $cpuinfo = file_get_contents('/proc/cpuinfo');
1519
            $count = substr_count($cpuinfo, 'processor');
1520
            if ($count > 0) {
1521
                return $count;
1522
            }
1523
        }
1524
1525
        throw new \LogicException('failed to detect number of CPUs!');
1526
    }
1527
1528
    /**
1529
     * @return array<string>
1530
     */
1531
    public static function getSupportedIssuesToFix(): array
1532
    {
1533
        return array_map(
1534
            /** @param class-string $issue_class */
0 ignored issues
show
Documentation introduced by
The doc-type class-string 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...
1535
            function (string $issue_class): string {
1536
                $parts = explode('\\', $issue_class);
1537
                return end($parts);
1538
            },
1539
            self::SUPPORTED_ISSUES_TO_FIX
1540
        );
1541
    }
1542
}
1543