Analyzer   F
last analyzed

Complexity

Total Complexity 244

Size/Duplication

Total Lines 1485
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 25

Importance

Changes 0
Metric Value
dl 0
loc 1485
rs 0.8
c 0
b 0
f 0
wmc 244
lcom 1
cbo 25

38 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A addFilesToAnalyze() 0 5 1
A addFilesToShowResults() 0 4 1
A setFilesToUpdate() 0 4 1
A canReportIssues() 0 4 1
A getFileAnalyzer() 0 16 2
F analyzeFiles() 0 72 11
F doAnalysis() 0 266 47
F loadCachedResults() 0 277 42
F shiftFileOffsets() 0 142 39
A getMixedMemberNames() 0 4 1
A addMixedMemberName() 0 4 1
A hasMixedMemberName() 0 4 1
A addMixedMemberNames() 0 13 3
A getMixedCountsForFile() 0 8 2
A setMixedCountsForFile() 0 4 1
A incrementMixedCount() 0 12 3
A decrementMixedCount() 0 12 3
A incrementNonMixedCount() 0 12 3
A getMixedCounts() 0 10 2
A addNodeType() 0 15 3
A addNodeArgument() 0 17 2
A addNodeReference() 0 11 2
A addOffsetReference() 0 11 2
A getTotalTypeCoverage() 0 20 4
B getTypeInferenceSummary() 0 31 6
B getNonMixedStats() 0 32 7
A disableMixedCounts() 0 4 1
A enableMixedCounts() 0 4 1
C updateFile() 0 65 10
B getExistingIssuesForFile() 0 18 7
D removeExistingDataForFile() 0 36 19
A getAnalyzedMethods() 0 4 1
B getFileMaps() 0 26 6
A getMapsForFile() 0 8 1
A getPossibleMethodParamTypes() 0 4 1
A setAnalyzedMethod() 0 4 2
A isMethodAlreadyAnalyzed() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like Analyzer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Analyzer, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Psalm\Internal\Codebase;
3
4
use function array_filter;
5
use function array_intersect_key;
6
use function array_merge;
7
use function count;
8
use function explode;
9
use InvalidArgumentException;
10
use function number_format;
11
use function pathinfo;
12
use PhpParser;
13
use function preg_replace;
14
use Psalm\Config;
15
use Psalm\FileManipulation;
16
use Psalm\Internal\Analyzer\IssueData;
17
use Psalm\Internal\Analyzer\FileAnalyzer;
18
use Psalm\Internal\Analyzer\ProjectAnalyzer;
19
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
20
use Psalm\Internal\FileManipulation\FunctionDocblockManipulator;
21
use Psalm\Internal\FileManipulation\PropertyDocblockManipulator;
22
use Psalm\Internal\Provider\FileProvider;
23
use Psalm\Internal\Provider\FileStorageProvider;
24
use Psalm\IssueBuffer;
25
use Psalm\Progress\Progress;
26
use function strpos;
27
use function substr;
28
use function usort;
29
use function array_diff_key;
30
use function array_values;
31
32
/**
33
 * @psalm-type  TaggedCodeType = array<int, array{0: int, 1: non-empty-string}>
34
 *
35
 * @psalm-type  FileMapType = array{
36
 *      0: TaggedCodeType,
37
 *      1: TaggedCodeType,
38
 *      2: array<int, array{0: int, 1: non-empty-string, 2: int}>
39
 * }
40
 *
41
 * @psalm-type  WorkerData = array{
42
 *      issues: array<string, list<IssueData>>,
43
 *      fixable_issue_counts: array<string, int>,
44
 *      nonmethod_references_to_classes: array<string, array<string,bool>>,
45
 *      method_references_to_classes: array<string, array<string,bool>>,
46
 *      file_references_to_class_members: array<string, array<string,bool>>,
47
 *      file_references_to_missing_class_members: array<string, array<string,bool>>,
48
 *      mixed_counts: array<string, array{0: int, 1: int}>,
49
 *      mixed_member_names: array<string, array<string, bool>>,
50
 *      file_manipulations: array<string, FileManipulation[]>,
51
 *      method_references_to_class_members: array<string, array<string,bool>>,
52
 *      method_references_to_missing_class_members: array<string, array<string,bool>>,
53
 *      method_param_uses: array<string, array<int, array<string, bool>>>,
54
 *      analyzed_methods: array<string, array<string, int>>,
55
 *      file_maps: array<string, FileMapType>,
56
 *      class_locations: array<string, array<int, \Psalm\CodeLocation>>,
57
 *      class_method_locations: array<string, array<int, \Psalm\CodeLocation>>,
58
 *      class_property_locations: array<string, array<int, \Psalm\CodeLocation>>,
59
 *      possible_method_param_types: array<string, array<int, \Psalm\Type\Union>>,
60
 *      taint_data: ?\Psalm\Internal\Codebase\Taint,
61
 *      unused_suppressions: array<string, array<int, int>>,
62
 *      used_suppressions: array<string, array<int, bool>>
63
 * }
64
 */
65
66
/**
67
 * @internal
68
 *
69
 * Called in the analysis phase of Psalm's execution
70
 */
71
class Analyzer
72
{
73
    /**
74
     * @var Config
75
     */
76
    private $config;
77
78
    /**
79
     * @var FileProvider
80
     */
81
    private $file_provider;
82
83
    /**
84
     * @var FileStorageProvider
85
     */
86
    private $file_storage_provider;
87
88
    /**
89
     * @var Progress
90
     */
91
    private $progress;
92
93
    /**
94
     * Used to store counts of mixed vs non-mixed variables
95
     *
96
     * @var array<string, array{0: int, 1: int}>
97
     */
98
    private $mixed_counts = [];
99
100
    /**
101
     * Used to store member names of mixed property/method access
102
     *
103
     * @var array<string, array<string, bool>>
104
     */
105
    private $mixed_member_names = [];
106
107
    /**
108
     * @var bool
109
     */
110
    private $count_mixed = true;
111
112
    /**
113
     * We analyze more files than we necessarily report errors in
114
     *
115
     * @var array<string, string>
116
     */
117
    private $files_to_analyze = [];
118
119
    /**
120
     * We can show analysis results on more files than we analyze
121
     * because the results can be cached
122
     *
123
     * @var array<string, string>
124
     */
125
    private $files_with_analysis_results = [];
126
127
    /**
128
     * We may update fewer files than we analyse (i.e. for dead code detection)
129
     *
130
     * @var array<string>|null
131
     */
132
    private $files_to_update = null;
133
134
    /**
135
     * @var array<string, array<string, int>>
136
     */
137
    private $analyzed_methods = [];
138
139
    /**
140
     * @var array<string, array<int, IssueData>>
141
     */
142
    private $existing_issues = [];
143
144
    /**
145
     * @var array<string, array<int, array{0: int, 1: non-empty-string}>>
146
     */
147
    private $reference_map = [];
148
149
    /**
150
     * @var array<string, array<int, array{0: int, 1: non-empty-string}>>
151
     */
152
    private $type_map = [];
153
154
    /**
155
     * @var array<string, array<int, array{0: int, 1: non-empty-string, 2: int}>>
156
     */
157
    private $argument_map = [];
158
159
    /**
160
     * @var array<string, array<int, \Psalm\Type\Union>>
161
     */
162
    public $possible_method_param_types = [];
163
164
    public function __construct(
165
        Config $config,
166
        FileProvider $file_provider,
167
        FileStorageProvider $file_storage_provider,
168
        Progress $progress
169
    ) {
170
        $this->config = $config;
171
        $this->file_provider = $file_provider;
172
        $this->file_storage_provider = $file_storage_provider;
173
        $this->progress = $progress;
174
    }
175
176
    /**
177
     * @param array<string, string> $files_to_analyze
178
     *
179
     * @return void
180
     */
181
    public function addFilesToAnalyze(array $files_to_analyze)
182
    {
183
        $this->files_to_analyze += $files_to_analyze;
184
        $this->files_with_analysis_results += $files_to_analyze;
185
    }
186
187
    /**
188
     * @param array<string, string> $files_to_analyze
189
     *
190
     * @return void
191
     */
192
    public function addFilesToShowResults(array $files_to_analyze)
193
    {
194
        $this->files_with_analysis_results += $files_to_analyze;
195
    }
196
197
    /**
198
     * @param array<string> $files_to_update
199
     *
200
     * @return void
201
     */
202
    public function setFilesToUpdate(array $files_to_update)
203
    {
204
        $this->files_to_update = $files_to_update;
205
    }
206
207
    /**
208
     * @param  string $file_path
209
     *
210
     * @return bool
211
     */
212
    public function canReportIssues($file_path)
213
    {
214
        return isset($this->files_with_analysis_results[$file_path]);
215
    }
216
217
    /**
218
     * @param  string $file_path
219
     * @param  array<string, class-string<FileAnalyzer>> $filetype_analyzers
220
     *
221
     * @return FileAnalyzer
222
     */
223
    private function getFileAnalyzer(ProjectAnalyzer $project_analyzer, $file_path, array $filetype_analyzers)
224
    {
225
        $extension = (string) (pathinfo($file_path)['extension'] ?? '');
226
227
        $file_name = $this->config->shortenFileName($file_path);
228
229
        if (isset($filetype_analyzers[$extension])) {
230
            $file_analyzer = new $filetype_analyzers[$extension]($project_analyzer, $file_path, $file_name);
231
        } else {
232
            $file_analyzer = new FileAnalyzer($project_analyzer, $file_path, $file_name);
233
        }
234
235
        $this->progress->debug('Getting ' . $file_path . "\n");
236
237
        return $file_analyzer;
238
    }
239
240
    /**
241
     * @param  ProjectAnalyzer $project_analyzer
242
     * @param  int            $pool_size
243
     * @param  bool           $alter_code
244
     *
245
     * @return void
246
     */
247
    public function analyzeFiles(
248
        ProjectAnalyzer $project_analyzer,
249
        int $pool_size,
250
        bool $alter_code,
251
        bool $consolidate_analyzed_data = false
252
    ) {
253
        $this->loadCachedResults($project_analyzer);
254
255
        $codebase = $project_analyzer->getCodebase();
256
257
        if ($alter_code) {
258
            $project_analyzer->interpretRefactors();
259
        }
260
261
        $this->files_to_analyze = array_filter(
262
            $this->files_to_analyze,
263
            function (string $file_path) : bool {
264
                return $this->file_provider->fileExists($file_path);
265
            }
266
        );
267
268
        $this->doAnalysis($project_analyzer, $pool_size);
269
270
        $scanned_files = $codebase->scanner->getScannedFiles();
271
272
        if ($codebase->taint) {
273
            $codebase->taint->connectSinksAndSources();
274
        }
275
276
        $this->progress->finish();
277
278
        if ($consolidate_analyzed_data) {
279
            $project_analyzer->consolidateAnalyzedData();
280
        }
281
282
        foreach (IssueBuffer::getIssuesData() as $file_path => $file_issues) {
283
            $codebase->file_reference_provider->clearExistingIssuesForFile($file_path);
284
285
            foreach ($file_issues as $issue_data) {
286
                $codebase->file_reference_provider->addIssue($file_path, $issue_data);
287
            }
288
        }
289
290
        $codebase->file_reference_provider->updateReferenceCache($codebase, $scanned_files);
291
292
        if ($codebase->track_unused_suppressions) {
293
            IssueBuffer::processUnusedSuppressions($codebase->file_provider);
294
        }
295
296
        $codebase->file_reference_provider->setAnalyzedMethods($this->analyzed_methods);
297
        $codebase->file_reference_provider->setFileMaps($this->getFileMaps());
298
        $codebase->file_reference_provider->setTypeCoverage($this->mixed_counts);
299
        $codebase->file_reference_provider->updateReferenceCache($codebase, $scanned_files);
300
301
        if ($codebase->diff_methods) {
302
            $codebase->statements_provider->resetDiffs();
303
        }
304
305
        if ($alter_code) {
306
            $this->progress->startAlteringFiles();
307
308
            $project_analyzer->prepareMigration();
309
310
            $files_to_update = $this->files_to_update !== null ? $this->files_to_update : $this->files_to_analyze;
311
312
            foreach ($files_to_update as $file_path) {
313
                $this->updateFile($file_path, $project_analyzer->dry_run);
314
            }
315
316
            $project_analyzer->migrateCode();
317
        }
318
    }
319
320
    private function doAnalysis(ProjectAnalyzer $project_analyzer, int $pool_size, bool $rerun = false) : void
321
    {
322
        $this->progress->start(count($this->files_to_analyze));
323
324
        \ksort($this->files_to_analyze);
325
326
        $codebase = $project_analyzer->getCodebase();
327
328
        $filetype_analyzers = $this->config->getFiletypeAnalyzers();
329
330
        $analysis_worker =
331
            /**
332
             * @param int $_
333
             * @param string $file_path
334
             *
335
             * @return array
336
             */
337
            function ($_, $file_path) use ($project_analyzer, $filetype_analyzers) {
338
                $file_analyzer = $this->getFileAnalyzer($project_analyzer, $file_path, $filetype_analyzers);
339
340
                $this->progress->debug('Analyzing ' . $file_analyzer->getFilePath() . "\n");
341
342
                $file_analyzer->analyze(null);
343
                $file_analyzer->context = null;
344
                $file_analyzer->clearSourceBeforeDestruction();
345
                unset($file_analyzer);
346
347
                return IssueBuffer::getIssuesDataForFile($file_path);
348
            };
349
350
        $task_done_closure =
351
            /**
352
             * @param array<IssueData> $issues
353
             */
354
            function (array $issues): void {
355
                $has_error = false;
356
                $has_info = false;
357
358
                foreach ($issues as $issue) {
359
                    if ($issue->severity === 'error') {
360
                        $has_error = true;
361
                        break;
362
                    }
363
364
                    if ($issue->severity === 'info') {
365
                        $has_info = true;
366
                    }
367
                }
368
369
                $this->progress->taskDone($has_error ? 2 : ($has_info ? 1 : 0));
370
            };
371
372
        if ($pool_size > 1 && count($this->files_to_analyze) > $pool_size) {
373
            $shuffle_count = $pool_size + 1;
374
375
            $file_paths = \array_values($this->files_to_analyze);
376
377
            $count = count($file_paths);
378
            $middle = \intdiv($count, $shuffle_count);
379
            $remainder = $count % $shuffle_count;
380
381
            $new_file_paths = [];
382
383
            for ($i = 0; $i < $shuffle_count; $i++) {
384
                for ($j = 0; $j < $middle; $j++) {
385
                    if ($j * $shuffle_count + $i < $count) {
386
                        $new_file_paths[] = $file_paths[$j * $shuffle_count + $i];
387
                    }
388
                }
389
390
                if ($remainder) {
391
                    $new_file_paths[] = $file_paths[$middle * $shuffle_count + $remainder - 1];
392
                    $remainder--;
393
                }
394
            }
395
396
            $process_file_paths = [];
397
398
            $i = 0;
399
400
            foreach ($file_paths as $file_path) {
401
                $process_file_paths[$i % $pool_size][] = $file_path;
402
                ++$i;
403
            }
404
405
            // Run analysis one file at a time, splitting the set of
406
            // files up among a given number of child processes.
407
            $pool = new \Psalm\Internal\Fork\Pool(
408
                $process_file_paths,
409
                /** @return void */
410
                function () {
411
                    $project_analyzer = ProjectAnalyzer::getInstance();
412
                    $codebase = $project_analyzer->getCodebase();
413
414
                    $file_reference_provider = $codebase->file_reference_provider;
415
416
                    if ($codebase->taint) {
417
                        $codebase->taint = new \Psalm\Internal\Codebase\Taint();
418
                    }
419
420
                    $file_reference_provider->setNonMethodReferencesToClasses([]);
421
                    $file_reference_provider->setCallingMethodReferencesToClassMembers([]);
422
                    $file_reference_provider->setFileReferencesToClassMembers([]);
423
                    $file_reference_provider->setCallingMethodReferencesToMissingClassMembers([]);
424
                    $file_reference_provider->setFileReferencesToMissingClassMembers([]);
425
                    $file_reference_provider->setReferencesToMixedMemberNames([]);
426
                    $file_reference_provider->setMethodParamUses([]);
427
                },
428
                $analysis_worker,
429
                /** @return WorkerData */
430
                function () use ($rerun) {
431
                    $project_analyzer = ProjectAnalyzer::getInstance();
432
                    $codebase = $project_analyzer->getCodebase();
433
                    $analyzer = $codebase->analyzer;
434
                    $file_reference_provider = $codebase->file_reference_provider;
435
436
                    $this->progress->debug('Gathering data for forked process' . "\n");
437
438
                    // @codingStandardsIgnoreStart
439
                    return [
440
                        'issues' => IssueBuffer::getIssuesData(),
441
                        'fixable_issue_counts' => IssueBuffer::getFixableIssues(),
442
                        'nonmethod_references_to_classes' => $rerun ? [] : $file_reference_provider->getAllNonMethodReferencesToClasses(),
443
                        'method_references_to_classes' => $rerun ? [] : $file_reference_provider->getAllMethodReferencesToClasses(),
444
                        'file_references_to_class_members' => $rerun ? [] : $file_reference_provider->getAllFileReferencesToClassMembers(),
445
                        'method_references_to_class_members' => $rerun ? [] : $file_reference_provider->getAllMethodReferencesToClassMembers(),
446
                        'file_references_to_missing_class_members' => $rerun ? [] : $file_reference_provider->getAllFileReferencesToMissingClassMembers(),
447
                        'method_references_to_missing_class_members' => $rerun ? [] : $file_reference_provider->getAllMethodReferencesToMissingClassMembers(),
448
                        'method_param_uses' => $rerun ? [] : $file_reference_provider->getAllMethodParamUses(),
449
                        'mixed_member_names' => $rerun ? [] : $analyzer->getMixedMemberNames(),
450
                        'file_manipulations' => $rerun ? [] : FileManipulationBuffer::getAll(),
451
                        'mixed_counts' => $rerun ? [] : $analyzer->getMixedCounts(),
452
                        'analyzed_methods' => $rerun ? [] : $analyzer->getAnalyzedMethods(),
453
                        'file_maps' => $rerun ? [] : $analyzer->getFileMaps(),
454
                        'class_locations' => $rerun ? [] : $file_reference_provider->getAllClassLocations(),
455
                        'class_method_locations' => $rerun ? [] : $file_reference_provider->getAllClassMethodLocations(),
456
                        'class_property_locations' => $rerun ? [] : $file_reference_provider->getAllClassPropertyLocations(),
457
                        'possible_method_param_types' => $rerun ? [] : $analyzer->getPossibleMethodParamTypes(),
458
                        'taint_data' => $codebase->taint,
459
                        'unused_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUnusedSuppressions() : [],
460
                        'used_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUsedSuppressions() : [],
461
                    ];
462
                    // @codingStandardsIgnoreEnd
463
                },
464
                $task_done_closure
465
            );
466
467
            $this->progress->debug('Forking analysis' . "\n");
468
469
            // Wait for all tasks to complete and collect the results.
470
            /**
471
             * @var array<int, WorkerData>
472
             */
473
            $forked_pool_data = $pool->wait();
474
475
            $this->progress->debug('Collecting forked analysis results' . "\n");
476
477
            foreach ($forked_pool_data as $pool_data) {
478
                IssueBuffer::addIssues($pool_data['issues']);
479
                IssueBuffer::addFixableIssues($pool_data['fixable_issue_counts']);
480
481
                if ($codebase->track_unused_suppressions) {
482
                    IssueBuffer::addUnusedSuppressions($pool_data['unused_suppressions']);
483
                    IssueBuffer::addUsedSuppressions($pool_data['used_suppressions']);
484
                }
485
486
                if ($codebase->taint && $pool_data['taint_data']) {
487
                    $codebase->taint->addThreadData($pool_data['taint_data']);
488
                }
489
490
                if ($rerun) {
491
                    continue;
492
                }
493
494
                $codebase->file_reference_provider->addNonMethodReferencesToClasses(
495
                    $pool_data['nonmethod_references_to_classes']
496
                );
497
                $codebase->file_reference_provider->addMethodReferencesToClasses(
498
                    $pool_data['method_references_to_classes']
499
                );
500
                $codebase->file_reference_provider->addFileReferencesToClassMembers(
501
                    $pool_data['file_references_to_class_members']
502
                );
503
                $codebase->file_reference_provider->addMethodReferencesToClassMembers(
504
                    $pool_data['method_references_to_class_members']
505
                );
506
                $codebase->file_reference_provider->addFileReferencesToMissingClassMembers(
507
                    $pool_data['file_references_to_missing_class_members']
508
                );
509
                $codebase->file_reference_provider->addMethodReferencesToMissingClassMembers(
510
                    $pool_data['method_references_to_missing_class_members']
511
                );
512
                $codebase->file_reference_provider->addMethodParamUses(
513
                    $pool_data['method_param_uses']
514
                );
515
                $this->addMixedMemberNames(
516
                    $pool_data['mixed_member_names']
517
                );
518
                $codebase->file_reference_provider->addClassLocations(
519
                    $pool_data['class_locations']
520
                );
521
                $codebase->file_reference_provider->addClassMethodLocations(
522
                    $pool_data['class_method_locations']
523
                );
524
                $codebase->file_reference_provider->addClassPropertyLocations(
525
                    $pool_data['class_property_locations']
526
                );
527
528
                $this->analyzed_methods = array_merge($pool_data['analyzed_methods'], $this->analyzed_methods);
529
530
                foreach ($pool_data['mixed_counts'] as $file_path => list($mixed_count, $nonmixed_count)) {
531
                    if (!isset($this->mixed_counts[$file_path])) {
532
                        $this->mixed_counts[$file_path] = [$mixed_count, $nonmixed_count];
533
                    } else {
534
                        $this->mixed_counts[$file_path][0] += $mixed_count;
535
                        $this->mixed_counts[$file_path][1] += $nonmixed_count;
536
                    }
537
                }
538
539
                foreach ($pool_data['possible_method_param_types'] as $declaring_method_id => $possible_param_types) {
540
                    if (!isset($this->possible_method_param_types[$declaring_method_id])) {
541
                        $this->possible_method_param_types[$declaring_method_id] = $possible_param_types;
542
                    } else {
543
                        foreach ($possible_param_types as $offset => $possible_param_type) {
544
                            if (!isset($this->possible_method_param_types[$declaring_method_id][$offset])) {
545
                                $this->possible_method_param_types[$declaring_method_id][$offset]
546
                                    = $possible_param_type;
547
                            } else {
548
                                $this->possible_method_param_types[$declaring_method_id][$offset]
549
                                    = \Psalm\Type::combineUnionTypes(
550
                                        $this->possible_method_param_types[$declaring_method_id][$offset],
551
                                        $possible_param_type,
552
                                        $codebase
553
                                    );
554
                            }
555
                        }
556
                    }
557
                }
558
559
                foreach ($pool_data['file_manipulations'] as $file_path => $manipulations) {
560
                    FileManipulationBuffer::add($file_path, $manipulations);
561
                }
562
563
                foreach ($pool_data['file_maps'] as $file_path => $file_maps) {
564
                    list($reference_map, $type_map, $argument_map) = $file_maps;
565
                    $this->reference_map[$file_path] = $reference_map;
566
                    $this->type_map[$file_path] = $type_map;
567
                    $this->argument_map[$file_path] = $argument_map;
568
                }
569
            }
570
571
            if ($pool->didHaveError()) {
572
                exit(1);
573
            }
574
        } else {
575
            $i = 0;
576
577
            foreach ($this->files_to_analyze as $file_path => $_) {
578
                $analysis_worker($i, $file_path);
579
                ++$i;
580
581
                $issues = IssueBuffer::getIssuesDataForFile($file_path);
582
                $task_done_closure($issues);
583
            }
584
        }
585
    }
586
587
    /**
588
     * @return void
589
     */
590
    public function loadCachedResults(ProjectAnalyzer $project_analyzer)
591
    {
592
        $codebase = $project_analyzer->getCodebase();
593
594
        if ($codebase->diff_methods) {
595
            $this->analyzed_methods = $codebase->file_reference_provider->getAnalyzedMethods();
596
            $this->existing_issues = $codebase->file_reference_provider->getExistingIssues();
597
            $file_maps = $codebase->file_reference_provider->getFileMaps();
598
599
            foreach ($file_maps as $file_path => list($reference_map, $type_map, $argument_map)) {
600
                $this->reference_map[$file_path] = $reference_map;
601
                $this->type_map[$file_path] = $type_map;
602
                $this->argument_map[$file_path] = $argument_map;
603
            }
604
        }
605
606
        $statements_provider = $codebase->statements_provider;
607
        $file_reference_provider = $codebase->file_reference_provider;
608
609
        $changed_members = $statements_provider->getChangedMembers();
610
        $unchanged_signature_members = $statements_provider->getUnchangedSignatureMembers();
611
612
        $diff_map = $statements_provider->getDiffMap();
613
614
        $method_references_to_class_members
615
            = $file_reference_provider->getAllMethodReferencesToClassMembers();
616
        $method_references_to_missing_class_members =
617
            $file_reference_provider->getAllMethodReferencesToMissingClassMembers();
618
619
        $all_referencing_methods = $method_references_to_class_members + $method_references_to_missing_class_members;
620
621
        $nonmethod_references_to_classes = $file_reference_provider->getAllNonMethodReferencesToClasses();
622
623
        $method_references_to_classes = $file_reference_provider->getAllMethodReferencesToClasses();
624
625
        $method_param_uses = $file_reference_provider->getAllMethodParamUses();
626
627
        $file_references_to_class_members
628
            = $file_reference_provider->getAllFileReferencesToClassMembers();
629
        $file_references_to_missing_class_members
630
            = $file_reference_provider->getAllFileReferencesToMissingClassMembers();
631
632
        $references_to_mixed_member_names = $file_reference_provider->getAllReferencesToMixedMemberNames();
633
634
        $this->mixed_counts = $file_reference_provider->getTypeCoverage();
635
636
        foreach ($changed_members as $file_path => $members_by_file) {
637
            foreach ($members_by_file as $changed_member => $_) {
638
                if (!strpos($changed_member, '&')) {
639
                    continue;
640
                }
641
642
                list($base_class, $trait) = explode('&', $changed_member);
643
644
                foreach ($all_referencing_methods as $member_id => $_) {
645
                    if (strpos($member_id, $base_class . '::') !== 0) {
646
                        continue;
647
                    }
648
649
                    $member_bit = substr($member_id, \strlen($base_class) + 2);
650
651
                    if (isset($all_referencing_methods[$trait . '::' . $member_bit])) {
652
                        $changed_members[$file_path][$member_id] = true;
653
                    }
654
                }
655
            }
656
        }
657
658
        $newly_invalidated_methods = [];
659
660
        foreach ($unchanged_signature_members as $file_unchanged_signature_members) {
661
            $newly_invalidated_methods = array_merge($newly_invalidated_methods, $file_unchanged_signature_members);
662
663
            foreach ($file_unchanged_signature_members as $unchanged_signature_member_id => $_) {
664
                // also check for things that might invalidate constructor property initialisation
665
                if (isset($all_referencing_methods[$unchanged_signature_member_id])) {
666
                    foreach ($all_referencing_methods[$unchanged_signature_member_id] as $referencing_method_id => $_) {
667
                        if (substr($referencing_method_id, -13) === '::__construct') {
668
                            $referencing_base_classlike = explode('::', $referencing_method_id)[0];
669
                            $unchanged_signature_classlike = explode('::', $unchanged_signature_member_id)[0];
670
671
                            if ($referencing_base_classlike === $unchanged_signature_classlike) {
672
                                $newly_invalidated_methods[$referencing_method_id] = true;
673
                            } else {
674
                                try {
675
                                    $referencing_storage = $codebase->classlike_storage_provider->get(
676
                                        $referencing_base_classlike
677
                                    );
678
                                } catch (InvalidArgumentException $_) {
679
                                    // Workaround for #3671
680
                                    $newly_invalidated_methods[$referencing_method_id] = true;
681
                                    $referencing_storage = null;
682
                                }
683
684
                                if (isset($referencing_storage->used_traits[$unchanged_signature_classlike])
685
                                    || isset($referencing_storage->parent_classes[$unchanged_signature_classlike])
686
                                ) {
687
                                    $newly_invalidated_methods[$referencing_method_id] = true;
688
                                }
689
                            }
690
                        }
691
                    }
692
                }
693
            }
694
        }
695
696
        foreach ($changed_members as $file_changed_members) {
697
            foreach ($file_changed_members as $member_id => $_) {
698
                $newly_invalidated_methods[$member_id] = true;
699
700
                if (isset($all_referencing_methods[$member_id])) {
701
                    $newly_invalidated_methods = array_merge(
702
                        $all_referencing_methods[$member_id],
703
                        $newly_invalidated_methods
704
                    );
705
                }
706
707
                unset(
708
                    $method_references_to_class_members[$member_id],
709
                    $file_references_to_class_members[$member_id],
710
                    $method_references_to_missing_class_members[$member_id],
711
                    $file_references_to_missing_class_members[$member_id],
712
                    $references_to_mixed_member_names[$member_id],
713
                    $method_param_uses[$member_id]
714
                );
715
716
                $member_stub = preg_replace('/::.*$/', '::*', $member_id);
717
718
                if (isset($all_referencing_methods[$member_stub])) {
719
                    $newly_invalidated_methods = array_merge(
720
                        $all_referencing_methods[$member_stub],
721
                        $newly_invalidated_methods
722
                    );
723
                }
724
            }
725
        }
726
727
        foreach ($newly_invalidated_methods as $method_id => $_) {
728
            foreach ($method_references_to_class_members as $i => $_) {
729
                unset($method_references_to_class_members[$i][$method_id]);
730
            }
731
732
            foreach ($method_references_to_classes as $i => $_) {
733
                unset($method_references_to_classes[$i][$method_id]);
734
            }
735
736
            foreach ($method_references_to_missing_class_members as $i => $_) {
737
                unset($method_references_to_missing_class_members[$i][$method_id]);
738
            }
739
740
            foreach ($references_to_mixed_member_names as $i => $_) {
741
                unset($references_to_mixed_member_names[$i][$method_id]);
742
            }
743
744
            foreach ($method_param_uses as $i => $_) {
745
                foreach ($method_param_uses[$i] as $j => $_) {
746
                    unset($method_param_uses[$i][$j][$method_id]);
747
                }
748
            }
749
        }
750
751
        foreach ($this->analyzed_methods as $file_path => $analyzed_methods) {
752
            foreach ($analyzed_methods as $correct_method_id => $_) {
753
                $trait_safe_method_id = $correct_method_id;
754
755
                $correct_method_ids = explode('&', $correct_method_id);
756
757
                $correct_method_id = $correct_method_ids[0];
758
759
                if (isset($newly_invalidated_methods[$correct_method_id])
760
                    || (isset($correct_method_ids[1])
761
                        && isset($newly_invalidated_methods[$correct_method_ids[1]]))
762
                ) {
763
                    unset($this->analyzed_methods[$file_path][$trait_safe_method_id]);
764
                }
765
            }
766
        }
767
768
        $this->shiftFileOffsets($diff_map);
769
770
        foreach ($this->files_to_analyze as $file_path) {
771
            $file_reference_provider->clearExistingIssuesForFile($file_path);
772
            $file_reference_provider->clearExistingFileMapsForFile($file_path);
773
774
            $this->setMixedCountsForFile($file_path, [0, 0]);
775
776
            foreach ($file_references_to_class_members as $i => $_) {
777
                unset($file_references_to_class_members[$i][$file_path]);
778
            }
779
780
            foreach ($nonmethod_references_to_classes as $i => $_) {
781
                unset($nonmethod_references_to_classes[$i][$file_path]);
782
            }
783
784
            foreach ($references_to_mixed_member_names as $i => $_) {
785
                unset($references_to_mixed_member_names[$i][$file_path]);
786
            }
787
788
            foreach ($file_references_to_missing_class_members as $i => $_) {
789
                unset($file_references_to_missing_class_members[$i][$file_path]);
790
            }
791
        }
792
793
        foreach ($this->existing_issues as $file_path => $issues) {
794
            if (!isset($this->files_to_analyze[$file_path])) {
795
                unset($this->existing_issues[$file_path]);
796
797
                if ($this->file_provider->fileExists($file_path)) {
798
                    IssueBuffer::addIssues([$file_path => array_values($issues)]);
799
                }
800
            }
801
        }
802
803
        $method_references_to_class_members = array_filter(
804
            $method_references_to_class_members
805
        );
806
807
        $method_references_to_missing_class_members = array_filter(
808
            $method_references_to_missing_class_members
809
        );
810
811
        $file_references_to_class_members = array_filter(
812
            $file_references_to_class_members
813
        );
814
815
        $file_references_to_missing_class_members = array_filter(
816
            $file_references_to_missing_class_members
817
        );
818
819
        $references_to_mixed_member_names = array_filter(
820
            $references_to_mixed_member_names
821
        );
822
823
        $nonmethod_references_to_classes = array_filter(
824
            $nonmethod_references_to_classes
825
        );
826
827
        $method_references_to_classes = array_filter(
828
            $method_references_to_classes
829
        );
830
831
        $method_param_uses = array_filter(
832
            $method_param_uses
833
        );
834
835
        $file_reference_provider->setCallingMethodReferencesToClassMembers(
836
            $method_references_to_class_members
837
        );
838
839
        $file_reference_provider->setFileReferencesToClassMembers(
840
            $file_references_to_class_members
841
        );
842
843
        $file_reference_provider->setCallingMethodReferencesToMissingClassMembers(
844
            $method_references_to_missing_class_members
845
        );
846
847
        $file_reference_provider->setFileReferencesToMissingClassMembers(
848
            $file_references_to_missing_class_members
849
        );
850
851
        $file_reference_provider->setReferencesToMixedMemberNames(
852
            $references_to_mixed_member_names
853
        );
854
855
        $file_reference_provider->setCallingMethodReferencesToClasses(
856
            $method_references_to_classes
857
        );
858
859
        $file_reference_provider->setNonMethodReferencesToClasses(
860
            $nonmethod_references_to_classes
861
        );
862
863
        $file_reference_provider->setMethodParamUses(
864
            $method_param_uses
865
        );
866
    }
867
868
    /**
869
     * @param array<string, array<int, array{int, int, int, int}>> $diff_map
870
     *
871
     * @return void
872
     */
873
    public function shiftFileOffsets(array $diff_map)
874
    {
875
        foreach ($this->existing_issues as $file_path => &$file_issues) {
876
            if (!isset($this->analyzed_methods[$file_path])) {
877
                continue;
878
            }
879
880
            $file_diff_map = $diff_map[$file_path] ?? [];
881
882
            if (!$file_diff_map) {
883
                continue;
884
            }
885
886
            $first_diff_offset = $file_diff_map[0][0];
887
            $last_diff_offset = $file_diff_map[count($file_diff_map) - 1][1];
888
889
            foreach ($file_issues as $i => &$issue_data) {
890
                if ($issue_data->to < $first_diff_offset || $issue_data->from > $last_diff_offset) {
891
                    unset($file_issues[$i]);
892
                    continue;
893
                }
894
895
                $matched = false;
896
897
                foreach ($file_diff_map as list($from, $to, $file_offset, $line_offset)) {
898
                    if ($issue_data->from >= $from
899
                        && $issue_data->from <= $to
900
                        && !$matched
901
                    ) {
902
                        $issue_data->from += $file_offset;
903
                        $issue_data->to += $file_offset;
904
                        $issue_data->snippet_from += $file_offset;
905
                        $issue_data->snippet_to += $file_offset;
906
                        $issue_data->line_from += $line_offset;
907
                        $issue_data->line_to += $line_offset;
908
                        $matched = true;
909
                    }
910
                }
911
912
                if (!$matched) {
913
                    unset($file_issues[$i]);
914
                }
915
            }
916
        }
917
918
        foreach ($this->reference_map as $file_path => &$reference_map) {
919
            if (!isset($this->analyzed_methods[$file_path])) {
920
                unset($this->reference_map[$file_path]);
921
                continue;
922
            }
923
924
            $file_diff_map = $diff_map[$file_path] ?? [];
925
926
            if (!$file_diff_map) {
927
                continue;
928
            }
929
930
            $first_diff_offset = $file_diff_map[0][0];
931
            $last_diff_offset = $file_diff_map[count($file_diff_map) - 1][1];
932
933
            foreach ($reference_map as $reference_from => list($reference_to, $tag)) {
934
                if ($reference_to < $first_diff_offset || $reference_from > $last_diff_offset) {
935
                    continue;
936
                }
937
938
                foreach ($file_diff_map as list($from, $to, $file_offset)) {
939
                    if ($reference_from >= $from && $reference_from <= $to) {
940
                        unset($reference_map[$reference_from]);
941
                        $reference_map[$reference_from += $file_offset] = [
942
                            $reference_to += $file_offset,
943
                            $tag,
944
                        ];
945
                    }
946
                }
947
            }
948
        }
949
950
        foreach ($this->type_map as $file_path => &$type_map) {
951
            if (!isset($this->analyzed_methods[$file_path])) {
952
                unset($this->type_map[$file_path]);
953
                continue;
954
            }
955
956
            $file_diff_map = $diff_map[$file_path] ?? [];
957
958
            if (!$file_diff_map) {
959
                continue;
960
            }
961
962
            $first_diff_offset = $file_diff_map[0][0];
963
            $last_diff_offset = $file_diff_map[count($file_diff_map) - 1][1];
964
965
            foreach ($type_map as $type_from => list($type_to, $tag)) {
966
                if ($type_to < $first_diff_offset || $type_from > $last_diff_offset) {
967
                    continue;
968
                }
969
970
                foreach ($file_diff_map as list($from, $to, $file_offset)) {
971
                    if ($type_from >= $from && $type_from <= $to) {
972
                        unset($type_map[$type_from]);
973
                        $type_map[$type_from += $file_offset] = [
974
                            $type_to += $file_offset,
975
                            $tag,
976
                        ];
977
                    }
978
                }
979
            }
980
        }
981
982
        foreach ($this->argument_map as $file_path => &$argument_map) {
983
            if (!isset($this->analyzed_methods[$file_path])) {
984
                unset($this->argument_map[$file_path]);
985
                continue;
986
            }
987
988
            $file_diff_map = $diff_map[$file_path] ?? [];
989
990
            if (!$file_diff_map) {
991
                continue;
992
            }
993
994
            $first_diff_offset = $file_diff_map[0][0];
995
            $last_diff_offset = $file_diff_map[count($file_diff_map) - 1][1];
996
997
            foreach ($argument_map as $argument_from => list($argument_to, $method_id, $argument_number)) {
998
                if ($argument_to < $first_diff_offset || $argument_from > $last_diff_offset) {
999
                    continue;
1000
                }
1001
1002
                foreach ($file_diff_map as list($from, $to, $file_offset)) {
1003
                    if ($argument_from >= $from && $argument_from <= $to) {
1004
                        unset($argument_map[$argument_from]);
1005
                        $argument_map[$argument_from += $file_offset] = [
1006
                            $argument_to += $file_offset,
1007
                            $method_id,
1008
                            $argument_number,
1009
                        ];
1010
                    }
1011
                }
1012
            }
1013
        }
1014
    }
1015
1016
    /**
1017
     * @return array<string, 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...
1018
     */
1019
    public function getMixedMemberNames() : array
1020
    {
1021
        return $this->mixed_member_names;
1022
    }
1023
1024
    /**
1025
     * @return void
1026
     */
1027
    public function addMixedMemberName(string $member_id, string $reference)
1028
    {
1029
        $this->mixed_member_names[$member_id][$reference] = true;
1030
    }
1031
1032
    public function hasMixedMemberName(string $member_id) : bool
1033
    {
1034
        return isset($this->mixed_member_names[$member_id]);
1035
    }
1036
1037
    /**
1038
     * @param array<string, array<string, bool>> $names
1039
     *
1040
     * @return void
1041
     */
1042
    public function addMixedMemberNames(array $names)
1043
    {
1044
        foreach ($names as $key => $name) {
1045
            if (isset($this->mixed_member_names[$key])) {
1046
                $this->mixed_member_names[$key] = array_merge(
1047
                    $this->mixed_member_names[$key],
1048
                    $name
1049
                );
1050
            } else {
1051
                $this->mixed_member_names[$key] = $name;
1052
            }
1053
        }
1054
    }
1055
1056
    /**
1057
     * @param  string $file_path
1058
     *
1059
     * @return array{0:int, 1:int}
0 ignored issues
show
Documentation introduced by
The doc-type array{0:int, could not be parsed: Unknown type name "array{0:int" 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...
1060
     */
1061
    public function getMixedCountsForFile($file_path)
1062
    {
1063
        if (!isset($this->mixed_counts[$file_path])) {
1064
            $this->mixed_counts[$file_path] = [0, 0];
1065
        }
1066
1067
        return $this->mixed_counts[$file_path];
1068
    }
1069
1070
    /**
1071
     * @param  string $file_path
1072
     * @param  array{0:int, 1:int} $mixed_counts
1073
     *
1074
     * @return void
1075
     */
1076
    public function setMixedCountsForFile($file_path, array $mixed_counts)
1077
    {
1078
        $this->mixed_counts[$file_path] = $mixed_counts;
1079
    }
1080
1081
    /**
1082
     * @param  string $file_path
1083
     *
1084
     * @return void
1085
     */
1086
    public function incrementMixedCount($file_path)
1087
    {
1088
        if (!$this->count_mixed) {
1089
            return;
1090
        }
1091
1092
        if (!isset($this->mixed_counts[$file_path])) {
1093
            $this->mixed_counts[$file_path] = [0, 0];
1094
        }
1095
1096
        ++$this->mixed_counts[$file_path][0];
1097
    }
1098
1099
    /**
1100
     * @param  string $file_path
1101
     *
1102
     * @return void
1103
     */
1104
    public function decrementMixedCount($file_path)
1105
    {
1106
        if (!$this->count_mixed) {
1107
            return;
1108
        }
1109
1110
        if (!isset($this->mixed_counts[$file_path])) {
1111
            return;
1112
        }
1113
1114
        --$this->mixed_counts[$file_path][0];
1115
    }
1116
1117
    /**
1118
     * @param  string $file_path
1119
     *
1120
     * @return void
1121
     */
1122
    public function incrementNonMixedCount($file_path)
1123
    {
1124
        if (!$this->count_mixed) {
1125
            return;
1126
        }
1127
1128
        if (!isset($this->mixed_counts[$file_path])) {
1129
            $this->mixed_counts[$file_path] = [0, 0];
1130
        }
1131
1132
        ++$this->mixed_counts[$file_path][1];
1133
    }
1134
1135
    /**
1136
     * @return array<string, array{0: int, 1: int}>
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...
1137
     */
1138
    public function getMixedCounts()
1139
    {
1140
        $all_deep_scanned_files = [];
1141
1142
        foreach ($this->files_to_analyze as $file_path => $_) {
1143
            $all_deep_scanned_files[$file_path] = true;
1144
        }
1145
1146
        return array_intersect_key($this->mixed_counts, $all_deep_scanned_files);
1147
    }
1148
1149
    /**
1150
     * @return void
1151
     */
1152
    public function addNodeType(
1153
        string $file_path,
1154
        PhpParser\Node $node,
1155
        string $node_type,
1156
        PhpParser\Node $parent_node = null
1157
    ) {
1158
        if (!$node_type) {
1159
            throw new \UnexpectedValueException('non-empty node_type expected');
1160
        }
1161
1162
        $this->type_map[$file_path][(int)$node->getAttribute('startFilePos')] = [
1163
            ($parent_node ? (int)$parent_node->getAttribute('endFilePos') : (int)$node->getAttribute('endFilePos')) + 1,
1164
            $node_type,
1165
        ];
1166
    }
1167
1168
    public function addNodeArgument(
1169
        string $file_path,
1170
        int $start_position,
1171
        int $end_position,
1172
        string $reference,
1173
        int $argument_number
1174
    ): void {
1175
        if (!$reference) {
1176
            throw new \UnexpectedValueException('non-empty node_type expected');
1177
        }
1178
1179
        $this->argument_map[$file_path][$start_position] = [
1180
            $end_position,
1181
            $reference,
1182
            $argument_number,
1183
        ];
1184
    }
1185
1186
    /**
1187
     * @return void
1188
     */
1189
    public function addNodeReference(string $file_path, PhpParser\Node $node, string $reference)
1190
    {
1191
        if (!$reference) {
1192
            throw new \UnexpectedValueException('non-empty node_type expected');
1193
        }
1194
1195
        $this->reference_map[$file_path][(int)$node->getAttribute('startFilePos')] = [
1196
            (int)$node->getAttribute('endFilePos') + 1,
1197
            $reference,
1198
        ];
1199
    }
1200
1201
    /**
1202
     * @return void
1203
     */
1204
    public function addOffsetReference(string $file_path, int $start, int $end, string $reference)
1205
    {
1206
        if (!$reference) {
1207
            throw new \UnexpectedValueException('non-empty node_type expected');
1208
        }
1209
1210
        $this->reference_map[$file_path][$start] = [
1211
            $end,
1212
            $reference,
1213
        ];
1214
    }
1215
1216
    /**
1217
     * @return array{int, int}
0 ignored issues
show
Documentation introduced by
The doc-type array{int, could not be parsed: Unknown type name "array{int" 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...
1218
     */
1219
    public function getTotalTypeCoverage(\Psalm\Codebase $codebase)
1220
    {
1221
        $mixed_count = 0;
1222
        $nonmixed_count = 0;
1223
1224
        foreach ($codebase->file_reference_provider->getTypeCoverage() as $file_path => $counts) {
1225
            if (!$this->config->reportTypeStatsForFile($file_path)) {
1226
                continue;
1227
            }
1228
1229
            list($path_mixed_count, $path_nonmixed_count) = $counts;
1230
1231
            if (isset($this->mixed_counts[$file_path])) {
1232
                $mixed_count += $path_mixed_count;
1233
                $nonmixed_count += $path_nonmixed_count;
1234
            }
1235
        }
1236
1237
        return [$mixed_count, $nonmixed_count];
1238
    }
1239
1240
    /**
1241
     * @return string
1242
     */
1243
    public function getTypeInferenceSummary(\Psalm\Codebase $codebase)
1244
    {
1245
        $all_deep_scanned_files = [];
1246
1247
        foreach ($this->files_to_analyze as $file_path => $_) {
1248
            $all_deep_scanned_files[$file_path] = true;
1249
1250
            foreach ($this->file_storage_provider->get($file_path)->required_file_paths as $required_file_path) {
1251
                $all_deep_scanned_files[$required_file_path] = true;
1252
            }
1253
        }
1254
1255
        list($mixed_count, $nonmixed_count) = $this->getTotalTypeCoverage($codebase);
1256
1257
        $total = $mixed_count + $nonmixed_count;
1258
1259
        $total_files = count($all_deep_scanned_files);
1260
1261
        if (!$total_files) {
1262
            return 'No files analyzed';
1263
        }
1264
1265
        if (!$total) {
1266
            return 'Psalm was unable to infer types in the codebase';
1267
        }
1268
1269
        $percentage = $nonmixed_count === $total ? '100' : number_format(100 * $nonmixed_count / $total, 4);
1270
1271
        return 'Psalm was able to infer types for ' . $percentage . '%'
1272
            . ' of the codebase';
1273
    }
1274
1275
    /**
1276
     * @return string
1277
     */
1278
    public function getNonMixedStats()
1279
    {
1280
        $stats = '';
1281
1282
        $all_deep_scanned_files = [];
1283
1284
        foreach ($this->files_to_analyze as $file_path => $_) {
1285
            $all_deep_scanned_files[$file_path] = true;
1286
1287
            if (!$this->config->reportTypeStatsForFile($file_path)) {
1288
                continue;
1289
            }
1290
1291
            foreach ($this->file_storage_provider->get($file_path)->required_file_paths as $required_file_path) {
1292
                $all_deep_scanned_files[$required_file_path] = true;
1293
            }
1294
        }
1295
1296
        foreach ($all_deep_scanned_files as $file_path => $_) {
1297
            if (isset($this->mixed_counts[$file_path])) {
1298
                list($path_mixed_count, $path_nonmixed_count) = $this->mixed_counts[$file_path];
1299
1300
                if ($path_mixed_count + $path_nonmixed_count) {
1301
                    $stats .= number_format(100 * $path_nonmixed_count / ($path_mixed_count + $path_nonmixed_count), 0)
1302
                        . '% ' . $this->config->shortenFileName($file_path)
1303
                        . ' (' . $path_mixed_count . ' mixed)' . "\n";
1304
                }
1305
            }
1306
        }
1307
1308
        return $stats;
1309
    }
1310
1311
    /**
1312
     * @return void
1313
     */
1314
    public function disableMixedCounts()
1315
    {
1316
        $this->count_mixed = false;
1317
    }
1318
1319
    /**
1320
     * @return void
1321
     */
1322
    public function enableMixedCounts()
1323
    {
1324
        $this->count_mixed = true;
1325
    }
1326
1327
    /**
1328
     * @param  string $file_path
1329
     * @param  bool $dry_run
1330
     *
1331
     * @return void
1332
     */
1333
    public function updateFile($file_path, $dry_run)
1334
    {
1335
        FileManipulationBuffer::add(
1336
            $file_path,
1337
            FunctionDocblockManipulator::getManipulationsForFile($file_path)
1338
        );
1339
1340
        FileManipulationBuffer::add(
1341
            $file_path,
1342
            PropertyDocblockManipulator::getManipulationsForFile($file_path)
1343
        );
1344
1345
        $file_manipulations = FileManipulationBuffer::getManipulationsForFile($file_path);
1346
1347
        if (!$file_manipulations) {
1348
            return;
1349
        }
1350
1351
        usort(
1352
            $file_manipulations,
1353
            /**
1354
             * @return int
1355
             */
1356
            function (FileManipulation $a, FileManipulation $b) {
1357
                if ($b->end === $a->end) {
1358
                    if ($a->start === $b->start) {
1359
                        return $b->insertion_text > $a->insertion_text ? 1 : -1;
1360
                    }
1361
1362
                    return $b->start > $a->start ? 1 : -1;
1363
                }
1364
1365
                return $b->end > $a->end ? 1 : -1;
1366
            }
1367
        );
1368
1369
        $last_start = \PHP_INT_MAX;
1370
        $existing_contents = $this->file_provider->getContents($file_path);
1371
1372
        foreach ($file_manipulations as $manipulation) {
1373
            if ($manipulation->start <= $last_start) {
1374
                $existing_contents = $manipulation->transform($existing_contents);
1375
                $last_start = $manipulation->start;
1376
            }
1377
        }
1378
1379
        if ($dry_run) {
1380
            echo $file_path . ':' . "\n";
1381
1382
            $differ = new \SebastianBergmann\Diff\Differ(
1383
                new \SebastianBergmann\Diff\Output\StrictUnifiedDiffOutputBuilder([
1384
                    'fromFile' => $file_path,
1385
                    'toFile' => $file_path,
1386
                ])
1387
            );
1388
1389
            echo $differ->diff($this->file_provider->getContents($file_path), $existing_contents);
1390
1391
            return;
1392
        }
1393
1394
        $this->progress->alterFileDone($file_path);
1395
1396
        $this->file_provider->setContents($file_path, $existing_contents);
1397
    }
1398
1399
    /**
1400
     * @param string $file_path
1401
     * @param int $start
1402
     * @param int $end
1403
     *
1404
     * @return list<IssueData>
0 ignored issues
show
Documentation introduced by
The doc-type list<IssueData> could not be parsed: Expected "|" or "end of type", but got "<" at position 4. (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...
1405
     */
1406
    public function getExistingIssuesForFile($file_path, $start, $end, ?string $issue_type = null)
1407
    {
1408
        if (!isset($this->existing_issues[$file_path])) {
1409
            return [];
1410
        }
1411
1412
        $applicable_issues = [];
1413
1414
        foreach ($this->existing_issues[$file_path] as $issue_data) {
1415
            if ($issue_data->from >= $start && $issue_data->from <= $end) {
1416
                if ($issue_type === null || $issue_type === $issue_data->type) {
1417
                    $applicable_issues[] = $issue_data;
1418
                }
1419
            }
1420
        }
1421
1422
        return $applicable_issues;
1423
    }
1424
1425
    /**
1426
     * @param string $file_path
1427
     * @param int $start
1428
     * @param int $end
1429
     *
1430
     * @return void
1431
     */
1432
    public function removeExistingDataForFile($file_path, $start, $end, ?string $issue_type = null)
1433
    {
1434
        if (isset($this->existing_issues[$file_path])) {
1435
            foreach ($this->existing_issues[$file_path] as $i => $issue_data) {
1436
                if ($issue_data->from >= $start && $issue_data->from <= $end) {
1437
                    if ($issue_type === null || $issue_type === $issue_data->type) {
1438
                        unset($this->existing_issues[$file_path][$i]);
1439
                    }
1440
                }
1441
            }
1442
        }
1443
1444
        if (isset($this->type_map[$file_path])) {
1445
            foreach ($this->type_map[$file_path] as $map_start => $_) {
1446
                if ($map_start >= $start && $map_start <= $end) {
1447
                    unset($this->type_map[$file_path][$map_start]);
1448
                }
1449
            }
1450
        }
1451
1452
        if (isset($this->reference_map[$file_path])) {
1453
            foreach ($this->reference_map[$file_path] as $map_start => $_) {
1454
                if ($map_start >= $start && $map_start <= $end) {
1455
                    unset($this->reference_map[$file_path][$map_start]);
1456
                }
1457
            }
1458
        }
1459
1460
        if (isset($this->argument_map[$file_path])) {
1461
            foreach ($this->argument_map[$file_path] as $map_start => $_) {
1462
                if ($map_start >= $start && $map_start <= $end) {
1463
                    unset($this->argument_map[$file_path][$map_start]);
1464
                }
1465
            }
1466
        }
1467
    }
1468
1469
    /**
1470
     * @return array<string, array<string, int>>
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...
1471
     */
1472
    public function getAnalyzedMethods()
1473
    {
1474
        return $this->analyzed_methods;
1475
    }
1476
1477
    /**
1478
     * @return array<string, FileMapType>
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...
1479
     */
1480
    public function getFileMaps()
1481
    {
1482
        $file_maps = [];
1483
1484
        foreach ($this->reference_map as $file_path => $reference_map) {
1485
            $file_maps[$file_path] = [$reference_map, [], []];
1486
        }
1487
1488
        foreach ($this->type_map as $file_path => $type_map) {
1489
            if (isset($file_maps[$file_path])) {
1490
                $file_maps[$file_path][1] = $type_map;
1491
            } else {
1492
                $file_maps[$file_path] = [[], $type_map, []];
1493
            }
1494
        }
1495
1496
        foreach ($this->argument_map as $file_path => $argument_map) {
1497
            if (isset($file_maps[$file_path])) {
1498
                $file_maps[$file_path][2] = $argument_map;
1499
            } else {
1500
                $file_maps[$file_path] = [[], [], $argument_map];
1501
            }
1502
        }
1503
1504
        return $file_maps;
1505
    }
1506
1507
    /**
1508
     * @return FileMapType
1509
     */
1510
    public function getMapsForFile(string $file_path)
1511
    {
1512
        return [
1513
            $this->reference_map[$file_path] ?? [],
1514
            $this->type_map[$file_path] ?? [],
1515
            $this->argument_map[$file_path] ?? [],
1516
        ];
1517
    }
1518
1519
    /**
1520
     * @return array<string, array<int, \Psalm\Type\Union>>
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...
1521
     */
1522
    public function getPossibleMethodParamTypes()
1523
    {
1524
        return $this->possible_method_param_types;
1525
    }
1526
1527
    /**
1528
     * @param string $file_path
1529
     * @param string $method_id
1530
     * @param bool $is_constructor
1531
     *
1532
     * @return void
1533
     */
1534
    public function setAnalyzedMethod($file_path, $method_id, $is_constructor = false)
1535
    {
1536
        $this->analyzed_methods[$file_path][$method_id] = $is_constructor ? 2 : 1;
1537
    }
1538
1539
    /**
1540
     * @param  string  $file_path
1541
     * @param  string  $method_id
1542
     * @param bool $is_constructor
1543
     *
1544
     * @return bool
1545
     */
1546
    public function isMethodAlreadyAnalyzed($file_path, $method_id, $is_constructor = false)
1547
    {
1548
        if ($is_constructor) {
1549
            return isset($this->analyzed_methods[$file_path][$method_id])
1550
                && $this->analyzed_methods[$file_path][$method_id] === 2;
1551
        }
1552
1553
        return isset($this->analyzed_methods[$file_path][$method_id]);
1554
    }
1555
}
1556