Issues (1236)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Psalm/IssueBuffer.php (15 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
namespace Psalm;
3
4
use function array_pop;
5
use function array_search;
6
use function array_splice;
7
use function count;
8
use function debug_print_backtrace;
9
use function explode;
10
use function file_put_contents;
11
use function fwrite;
12
use function get_class;
13
use function memory_get_peak_usage;
14
use function microtime;
15
use function number_format;
16
use function ob_get_clean;
17
use function ob_start;
18
use Psalm\Internal\Analyzer\IssueData;
19
use Psalm\Internal\Analyzer\ProjectAnalyzer;
20
use Psalm\Issue\CodeIssue;
21
use Psalm\Issue\UnusedPsalmSuppress;
22
use Psalm\Report\CheckstyleReport;
23
use Psalm\Report\CompactReport;
24
use Psalm\Report\ConsoleReport;
25
use Psalm\Report\EmacsReport;
26
use Psalm\Report\GithubActionsReport;
27
use Psalm\Report\JsonReport;
28
use Psalm\Report\JsonSummaryReport;
29
use Psalm\Report\JunitReport;
30
use Psalm\Report\PylintReport;
31
use Psalm\Report\SonarqubeReport;
32
use Psalm\Report\TextReport;
33
use Psalm\Report\XmlReport;
34
use function sha1;
35
use function str_repeat;
36
use function str_replace;
37
use function usort;
38
use function array_merge;
39
use function array_values;
40
use const DEBUG_BACKTRACE_IGNORE_ARGS;
41
use const STDERR;
42
43
class IssueBuffer
44
{
45
    /**
46
     * @var array<string, list<IssueData>>
47
     */
48
    protected static $issues_data = [];
49
50
    /**
51
     * @var array<int, array>
52
     */
53
    protected static $console_issues = [];
54
55
    /**
56
     * @var array<string, int>
57
     */
58
    protected static $fixable_issue_counts = [];
59
60
    /**
61
     * @var int
62
     */
63
    protected static $error_count = 0;
64
65
    /**
66
     * @var array<string, bool>
67
     */
68
    protected static $emitted = [];
69
70
    /** @var int */
71
    protected static $recording_level = 0;
72
73
    /** @var array<int, array<int, CodeIssue>> */
74
    protected static $recorded_issues = [];
75
76
    /**
77
     * @var array<string, array<int, int>>
78
     */
79
    protected static $unused_suppressions = [];
80
81
    /**
82
     * @var array<string, array<int, bool>>
83
     */
84
    protected static $used_suppressions = [];
85
86
    /**
87
     * @param   CodeIssue $e
88
     * @param   string[]  $suppressed_issues
89
     *
90
     * @return  bool
91
     */
92
    public static function accepts(CodeIssue $e, array $suppressed_issues = [], bool $is_fixable = false)
93
    {
94
        if (self::isSuppressed($e, $suppressed_issues)) {
95
            return false;
96
        }
97
98
        return self::add($e, $is_fixable);
99
    }
100
101
    public static function addUnusedSuppression(string $file_path, int $offset, string $issue_type) : void
102
    {
103
        if ($issue_type === 'TaintedInput') {
104
            return;
105
        }
106
107
        if (isset(self::$used_suppressions[$file_path][$offset])) {
108
            return;
109
        }
110
111
        if (!isset(self::$unused_suppressions[$file_path])) {
112
            self::$unused_suppressions[$file_path] = [];
113
        }
114
115
        self::$unused_suppressions[$file_path][$offset] = $offset + \strlen($issue_type) - 1;
116
    }
117
118
    /**
119
     * @param   CodeIssue $e
120
     * @param   string[]  $suppressed_issues
121
     *
122
     * @return  bool
123
     */
124
    public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) : bool
125
    {
126
        $config = Config::getInstance();
127
128
        $fqcn_parts = explode('\\', get_class($e));
129
        $issue_type = array_pop($fqcn_parts);
130
        $file_path = $e->getFilePath();
131
132
        if (!$config->reportIssueInFile($issue_type, $file_path)) {
133
            return true;
134
        }
135
136
        $suppressed_issue_position = array_search($issue_type, $suppressed_issues);
137
138 View Code Duplication
        if ($suppressed_issue_position !== false) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
139
            if (\is_int($suppressed_issue_position)) {
140
                self::$used_suppressions[$file_path][$suppressed_issue_position] = true;
141
            }
142
143
            return true;
144
        }
145
146
        $parent_issue_type = Config::getParentIssueType($issue_type);
147
148
        if ($parent_issue_type) {
149
            $suppressed_issue_position = array_search($parent_issue_type, $suppressed_issues);
150
151 View Code Duplication
            if ($suppressed_issue_position !== false) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
152
                if (\is_int($suppressed_issue_position)) {
153
                    self::$used_suppressions[$file_path][$suppressed_issue_position] = true;
154
                }
155
156
                return true;
157
            }
158
        }
159
160
        $suppress_all_position = array_search('all', $suppressed_issues);
161
162 View Code Duplication
        if ($suppress_all_position !== false) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
163
            if (\is_int($suppress_all_position)) {
164
                self::$used_suppressions[$file_path][$suppress_all_position] = true;
165
            }
166
167
            return true;
168
        }
169
170
        $reporting_level = $config->getReportingLevelForIssue($e);
171
172
        if ($reporting_level === Config::REPORT_SUPPRESS) {
173
            return true;
174
        }
175
176
        if ($e->getLocation()->getLineNumber() === -1) {
177
            return true;
178
        }
179
180
        if (self::$recording_level > 0) {
181
            self::$recorded_issues[self::$recording_level][] = $e;
182
183
            return true;
184
        }
185
186
        return false;
187
    }
188
189
    /**
190
     * @param   CodeIssue $e
191
     *
192
     * @throws  Exception\CodeException
193
     *
194
     * @return  bool
195
     */
196
    public static function add(CodeIssue $e, bool $is_fixable = false)
197
    {
198
        $config = Config::getInstance();
199
200
        $fqcn_parts = explode('\\', get_class($e));
201
        $issue_type = array_pop($fqcn_parts);
202
203
        $project_analyzer = ProjectAnalyzer::getInstance();
204
205
        if (!$project_analyzer->show_issues) {
206
            return false;
207
        }
208
209
        if ($project_analyzer->getCodebase()->taint && $issue_type !== 'TaintedInput') {
210
            return false;
211
        }
212
213
        $reporting_level = $config->getReportingLevelForIssue($e);
214
215
        if ($reporting_level === Config::REPORT_SUPPRESS) {
216
            return false;
217
        }
218
219
        if ($config->debug_emitted_issues) {
220
            ob_start();
221
            debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
222
            $trace = ob_get_clean();
223
            fwrite(STDERR, "\nEmitting {$e->getShortLocation()} $issue_type {$e->getMessage()}\n$trace\n");
224
        }
225
226
        $emitted_key = $issue_type . '-' . $e->getShortLocation() . ':' . $e->getLocation()->getColumn();
227
228 View Code Duplication
        if ($reporting_level === Config::REPORT_INFO) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
229
            if ($issue_type === 'TaintedInput' || !self::alreadyEmitted($emitted_key)) {
230
                self::$issues_data[$e->getFilePath()][] = $e->toIssueData(Config::REPORT_INFO);
231
            }
232
233
            return false;
234
        }
235
236
        if ($config->throw_exception) {
237
            \Psalm\Internal\Analyzer\FileAnalyzer::clearCache();
238
239
            $message = $e instanceof \Psalm\Issue\TaintedInput
240
                ? $e->getJourneyMessage()
241
                : $e->getMessage();
242
243
            throw new Exception\CodeException(
244
                $issue_type
245
                    . ' - ' . $e->getShortLocationWithPrevious()
246
                    . ':' . $e->getLocation()->getColumn()
247
                    . ' - ' . $message
248
            );
249
        }
250
251 View Code Duplication
        if ($issue_type === 'TaintedInput' || !self::alreadyEmitted($emitted_key)) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
252
            ++self::$error_count;
253
            self::$issues_data[$e->getFilePath()][] = $e->toIssueData(Config::REPORT_ERROR);
254
        }
255
256
        if ($is_fixable) {
257
            self::addFixableIssue($issue_type);
258
        }
259
260
        return true;
261
    }
262
263
    public static function remove(string $file_path, string $issue_type, int $file_offset) : void
264
    {
265
        if (!isset(self::$issues_data[$file_path])) {
266
            return;
267
        }
268
269
        $filtered_issues = [];
270
271
        foreach (self::$issues_data[$file_path] as $issue) {
272
            if ($issue->type !== $issue_type || $issue->from !== $file_offset) {
273
                $filtered_issues[] = $issue;
274
            }
275
        }
276
277
        if (empty($filtered_issues)) {
278
            unset(self::$issues_data[$file_path]);
279
        } else {
280
            self::$issues_data[$file_path] = $filtered_issues;
281
        }
282
    }
283
284
    public static function addFixableIssue(string $issue_type) : void
285
    {
286 View Code Duplication
        if (isset(self::$fixable_issue_counts[$issue_type])) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
287
            self::$fixable_issue_counts[$issue_type]++;
288
        } else {
289
            self::$fixable_issue_counts[$issue_type] = 1;
290
        }
291
    }
292
293
    /**
294
     * @return array<string, list<IssueData>>
0 ignored issues
show
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...
295
     */
296
    public static function getIssuesData()
297
    {
298
        return self::$issues_data;
299
    }
300
301
    /**
302
     * @return list<IssueData>
0 ignored issues
show
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...
303
     */
304
    public static function getIssuesDataForFile(string $file_path)
305
    {
306
        return self::$issues_data[$file_path] ?? [];
307
    }
308
309
    /**
310
     * @return array<string, int>
0 ignored issues
show
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...
311
     */
312
    public static function getFixableIssues()
313
    {
314
        return self::$fixable_issue_counts;
315
    }
316
317
    /**
318
     * @param array<string, int> $fixable_issue_counts
319
     */
320
    public static function addFixableIssues(array $fixable_issue_counts) : void
321
    {
322
        foreach ($fixable_issue_counts as $issue_type => $count) {
323 View Code Duplication
            if (isset(self::$fixable_issue_counts[$issue_type])) {
0 ignored issues
show
This code seems to be duplicated across your project.

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

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

Loading history...
324
                self::$fixable_issue_counts[$issue_type] += $count;
325
            } else {
326
                self::$fixable_issue_counts[$issue_type] = $count;
327
            }
328
        }
329
    }
330
331
    /**
332
     * @return array<string, array<int, int>>
0 ignored issues
show
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...
333
     */
334
    public static function getUnusedSuppressions() : array
335
    {
336
        return self::$unused_suppressions;
337
    }
338
339
    /**
340
     * @return array<string, array<int, bool>>
0 ignored issues
show
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...
341
     */
342
    public static function getUsedSuppressions() : array
343
    {
344
        return self::$used_suppressions;
345
    }
346
347
    /**
348
     * @param array<string, array<int, int>> $unused_suppressions
349
     */
350
    public static function addUnusedSuppressions(array $unused_suppressions) : void
351
    {
352
        self::$unused_suppressions += $unused_suppressions;
353
    }
354
355
    /**
356
     * @param array<string, array<int, bool>> $used_suppressions
357
     */
358
    public static function addUsedSuppressions(array $used_suppressions) : void
359
    {
360
        foreach ($used_suppressions as $file => $offsets) {
361
            if (!isset(self::$used_suppressions[$file])) {
362
                self::$used_suppressions[$file] = $offsets;
363
            } else {
364
                self::$used_suppressions[$file] += $offsets;
365
            }
366
        }
367
    }
368
369
    public static function processUnusedSuppressions(\Psalm\Internal\Provider\FileProvider $file_provider) : void
370
    {
371
        $config = Config::getInstance();
372
373
        foreach (self::$unused_suppressions as $file_path => $offsets) {
374
            if (!$offsets) {
375
                continue;
376
            }
377
378
            $file_contents = $file_provider->getContents($file_path);
379
380
            foreach ($offsets as $start => $end) {
381
                if (isset(self::$used_suppressions[$file_path][$start])) {
382
                    continue;
383
                }
384
385
                self::add(
386
                    new UnusedPsalmSuppress(
387
                        'This suppression is never used',
388
                        new CodeLocation\Raw(
389
                            $file_contents,
390
                            $file_path,
391
                            $config->shortenFileName($file_path),
392
                            $start,
393
                            $end
394
                        )
395
                    )
396
                );
397
            }
398
        }
399
    }
400
401
    /**
402
     * @return int
403
     */
404
    public static function getErrorCount()
405
    {
406
        return self::$error_count;
407
    }
408
409
    /**
410
     * @param array<string, list<IssueData>> $issues_data
411
     *
412
     * @return void
413
     */
414
    public static function addIssues(array $issues_data)
415
    {
416
        foreach ($issues_data as $file_path => $file_issues) {
417
            foreach ($file_issues as $issue) {
418
                $emitted_key = $issue->type
419
                    . '-' . $issue->file_name
420
                    . ':' . $issue->line_from
421
                    . ':' . $issue->column_from;
422
423
                if (!self::alreadyEmitted($emitted_key)) {
424
                    self::$issues_data[$file_path][] = $issue;
425
                }
426
            }
427
        }
428
    }
429
430
    /**
431
     * @param  ProjectAnalyzer                   $project_analyzer
432
     * @param  bool                             $is_full
433
     * @param  float                            $start_time
434
     * @param  bool                             $add_stats
435
     * @param  array<string,array<string,array{o:int, s:array<int, string>}>>  $issue_baseline
436
     *
437
     * @return void
438
     */
439
    public static function finish(
440
        ProjectAnalyzer $project_analyzer,
441
        bool $is_full,
442
        float $start_time,
443
        bool $add_stats = false,
444
        array $issue_baseline = []
445
    ) {
446
        if (!$project_analyzer->stdout_report_options) {
447
            throw new \UnexpectedValueException('Cannot finish without stdout report options');
448
        }
449
450
        $codebase = $project_analyzer->getCodebase();
451
452
        $error_count = 0;
453
        $info_count = 0;
454
455
        $issues_data = [];
456
457
        if (self::$issues_data) {
458
            if ($project_analyzer->stdout_report_options->format === Report::TYPE_CONSOLE) {
459
                echo "\n";
460
            }
461
462
            \ksort(self::$issues_data);
463
464
            foreach (self::$issues_data as $file_path => $file_issues) {
465
                usort(
466
                    $file_issues,
467
                    function (IssueData $d1, IssueData $d2) : int {
468
                        if ($d1->file_path === $d2->file_path) {
469
                            if ($d1->line_from === $d2->line_from) {
470
                                if ($d1->column_from === $d2->column_from) {
471
                                    return 0;
472
                                }
473
474
                                return $d1->column_from > $d2->column_from ? 1 : -1;
475
                            }
476
477
                            return $d1->line_from > $d2->line_from ? 1 : -1;
478
                        }
479
480
                        return $d1->file_path > $d2->file_path ? 1 : -1;
481
                    }
482
                );
483
                self::$issues_data[$file_path] = $file_issues;
484
            }
485
486
            // make a copy so what gets saved in cache is unaffected by baseline
487
            $issues_data = self::$issues_data;
488
489
            if (!empty($issue_baseline)) {
490
                // Set severity for issues in baseline to INFO
491
                foreach ($issues_data as $file_path => $file_issues) {
492
                    foreach ($file_issues as $key => $issue_data) {
493
                        $file = $issue_data->file_name;
494
                        $file = str_replace('\\', '/', $file);
495
                        $type = $issue_data->type;
496
497
                        if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) {
498
                            if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) {
499
                                $position = array_search(
500
                                    $issue_data->selected_text,
501
                                    $issue_baseline[$file][$type]['s'],
502
                                    true
503
                                );
504
505
                                if ($position !== false) {
506
                                    $issue_data->severity = Config::REPORT_INFO;
507
                                    array_splice($issue_baseline[$file][$type]['s'], $position, 1);
508
                                    $issue_baseline[$file][$type]['o'] = $issue_baseline[$file][$type]['o'] - 1;
509
                                }
510
                            } else {
511
                                $issue_baseline[$file][$type]['s'] = [];
512
                                $issue_data->severity = Config::REPORT_INFO;
513
                                $issue_baseline[$file][$type]['o'] = $issue_baseline[$file][$type]['o'] - 1;
514
                            }
515
                        }
516
517
                        /** @psalm-suppress PropertyTypeCoercion due to Psalm bug */
518
                        $issues_data[$file_path][$key] = $issue_data;
519
                    }
520
                }
521
            }
522
        }
523
524
        echo self::getOutput(
525
            $issues_data,
526
            $project_analyzer->stdout_report_options,
527
            $codebase->analyzer->getTotalTypeCoverage($codebase)
528
        );
529
530
        foreach ($issues_data as $file_issues) {
531
            foreach ($file_issues as $issue_data) {
532
                if ($issue_data->severity === Config::REPORT_ERROR) {
533
                    ++$error_count;
534
                } else {
535
                    ++$info_count;
536
                }
537
            }
538
        }
539
540
        $after_analysis_hooks = $codebase->config->after_analysis;
541
542
        if ($after_analysis_hooks) {
543
            $source_control_info = null;
544
            $build_info = (new \Psalm\Internal\ExecutionEnvironment\BuildInfoCollector($_SERVER))->collect();
545
546
            try {
547
                $source_control_info = (new \Psalm\Internal\ExecutionEnvironment\GitInfoCollector())->collect();
548
            } catch (\RuntimeException $e) {
549
                // do nothing
550
            }
551
552
            foreach ($after_analysis_hooks as $after_analysis_hook) {
553
                /** @psalm-suppress ArgumentTypeCoercion due to Psalm bug */
554
                $after_analysis_hook::afterAnalysis(
555
                    $codebase,
556
                    $issues_data,
557
                    $build_info,
558
                    $source_control_info
559
                );
560
            }
561
        }
562
563
        foreach ($project_analyzer->generated_report_options as $report_options) {
564
            if (!$report_options->output_path) {
565
                throw new \UnexpectedValueException('Output path should not be null here');
566
            }
567
568
            file_put_contents(
569
                $report_options->output_path,
570
                self::getOutput(
571
                    $issues_data,
572
                    $report_options,
573
                    $codebase->analyzer->getTotalTypeCoverage($codebase)
574
                )
575
            );
576
        }
577
578
        if ($project_analyzer->stdout_report_options->format === Report::TYPE_CONSOLE) {
579
            echo str_repeat('-', 30) . "\n";
580
581
            if ($error_count) {
582
                echo($project_analyzer->stdout_report_options->use_color
583
                    ? "\e[0;31m" . $error_count . " errors\e[0m"
584
                    : $error_count . ' errors'
585
                ) . ' found' . "\n";
586
            } else {
587
                echo 'No errors found!' . "\n";
588
            }
589
590
            $show_info = $project_analyzer->stdout_report_options->show_info;
591
            $show_suggestions = $project_analyzer->stdout_report_options->show_suggestions;
592
593
            if ($info_count && ($show_info || $show_suggestions)) {
594
                echo str_repeat('-', 30) . "\n";
595
596
                echo $info_count . ' other issues found.' . "\n";
597
598
                if (!$show_info) {
599
                    echo 'You can display them with ' .
600
                        ($project_analyzer->stdout_report_options->use_color
601
                            ? "\e[30;48;5;195m--show-info=true\e[0m"
602
                            : '--show-info=true') . "\n";
603
                }
604
            }
605
606
            if (self::$fixable_issue_counts && $show_suggestions && !$codebase->taint) {
607
                echo str_repeat('-', 30) . "\n";
608
609
                $total_count = \array_sum(self::$fixable_issue_counts);
610
                $command = '--alter --issues=' . \implode(',', \array_keys(self::$fixable_issue_counts));
611
                $command .= ' --dry-run';
612
613
                echo 'Psalm can automatically fix ' . $total_count
614
                    . ($show_info ? ' issues' : ' of these issues') . ".\n"
615
                    . 'Run Psalm again with ' . "\n"
616
                    . ($project_analyzer->stdout_report_options->use_color
617
                        ? "\e[30;48;5;195m" . $command . "\e[0m"
618
                        : $command) . "\n"
619
                    . 'to see what it can fix.' . "\n";
620
            }
621
622
            echo str_repeat('-', 30) . "\n" . "\n";
623
624
            if ($start_time) {
625
                echo 'Checks took ' . number_format(microtime(true) - $start_time, 2) . ' seconds';
626
                echo ' and used ' . number_format(memory_get_peak_usage() / (1024 * 1024), 3) . 'MB of memory' . "\n";
627
628
                $analysis_summary = $codebase->analyzer->getTypeInferenceSummary($codebase);
629
                echo $analysis_summary . "\n";
630
631
                if ($add_stats) {
632
                    echo '-----------------' . "\n";
633
                    echo $codebase->analyzer->getNonMixedStats();
634
                    echo "\n";
635
                }
636
            }
637
        }
638
639
        if ($is_full && $start_time) {
640
            $codebase->file_reference_provider->removeDeletedFilesFromReferences();
641
642
            if ($project_analyzer->project_cache_provider) {
643
                $project_analyzer->project_cache_provider->processSuccessfulRun($start_time);
644
            }
645
646
            if ($codebase->statements_provider->parser_cache_provider) {
647
                $codebase->statements_provider->parser_cache_provider->processSuccessfulRun();
648
            }
649
        }
650
651
        if ($error_count) {
652
            exit(1);
653
        }
654
    }
655
656
    /**
657
     * @param array<string, array<int, IssueData>> $issues_data
658
     * @param array{int, int} $mixed_counts
659
     *
660
     * @return string
661
     */
662
    public static function getOutput(
663
        array $issues_data,
664
        \Psalm\Report\ReportOptions $report_options,
665
        array $mixed_counts = [0, 0]
666
    ) {
667
        $total_expression_count = $mixed_counts[0] + $mixed_counts[1];
668
        $mixed_expression_count = $mixed_counts[0];
669
670
        $normalized_data = $issues_data === [] ? [] : array_merge(...array_values($issues_data));
671
672
        switch ($report_options->format) {
673
            case Report::TYPE_COMPACT:
674
                $output = new CompactReport($normalized_data, self::$fixable_issue_counts, $report_options);
675
                break;
676
677
            case Report::TYPE_EMACS:
678
                $output = new EmacsReport($normalized_data, self::$fixable_issue_counts, $report_options);
679
                break;
680
681
            case Report::TYPE_TEXT:
682
                $output = new TextReport($normalized_data, self::$fixable_issue_counts, $report_options);
683
                break;
684
685
            case Report::TYPE_JSON:
686
                $output = new JsonReport($normalized_data, self::$fixable_issue_counts, $report_options);
687
                break;
688
689
            case Report::TYPE_JSON_SUMMARY:
690
                $output = new JsonSummaryReport(
691
                    $normalized_data,
692
                    self::$fixable_issue_counts,
693
                    $report_options,
694
                    $mixed_expression_count,
695
                    $total_expression_count
696
                );
697
                break;
698
699
            case Report::TYPE_SONARQUBE:
700
                $output = new SonarqubeReport($normalized_data, self::$fixable_issue_counts, $report_options);
701
                break;
702
703
            case Report::TYPE_PYLINT:
704
                $output = new PylintReport($normalized_data, self::$fixable_issue_counts, $report_options);
705
                break;
706
707
            case Report::TYPE_CHECKSTYLE:
708
                $output = new CheckstyleReport($normalized_data, self::$fixable_issue_counts, $report_options);
709
                break;
710
711
            case Report::TYPE_XML:
712
                $output = new XmlReport($normalized_data, self::$fixable_issue_counts, $report_options);
713
                break;
714
715
            case Report::TYPE_JUNIT:
716
                $output = new JUnitReport($normalized_data, self::$fixable_issue_counts, $report_options);
717
                break;
718
719
            case Report::TYPE_CONSOLE:
720
                $output = new ConsoleReport($normalized_data, self::$fixable_issue_counts, $report_options);
721
                break;
722
723
            case Report::TYPE_GITHUB_ACTIONS:
724
                $output = new GithubActionsReport($normalized_data, self::$fixable_issue_counts, $report_options);
725
                break;
726
        }
727
728
        return $output->create();
0 ignored issues
show
The variable $output does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
729
    }
730
731
    /**
732
     * @param  string $message
733
     *
734
     * @return bool
735
     */
736
    protected static function alreadyEmitted($message)
737
    {
738
        $sham = sha1($message);
739
740
        if (isset(self::$emitted[$sham])) {
741
            return true;
742
        }
743
744
        self::$emitted[$sham] = true;
745
746
        return false;
747
    }
748
749
    /**
750
     * @return void
751
     */
752
    public static function clearCache()
753
    {
754
        self::$issues_data = [];
755
        self::$emitted = [];
756
        self::$error_count = 0;
757
        self::$recording_level = 0;
758
        self::$recorded_issues = [];
759
        self::$console_issues = [];
760
        self::$unused_suppressions = [];
761
        self::$used_suppressions = [];
762
    }
763
764
    /**
765
     * @return array<string, list<IssueData>>
0 ignored issues
show
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...
766
     */
767
    public static function clear()
768
    {
769
        $current_data = self::$issues_data;
770
        self::$issues_data = [];
771
        self::$emitted = [];
772
773
        return $current_data;
774
    }
775
776
    /**
777
     * @return bool
778
     */
779
    public static function isRecording()
780
    {
781
        return self::$recording_level > 0;
782
    }
783
784
    /**
785
     * @return void
786
     */
787
    public static function startRecording()
788
    {
789
        ++self::$recording_level;
790
        self::$recorded_issues[self::$recording_level] = [];
791
    }
792
793
    /**
794
     * @return void
795
     */
796
    public static function stopRecording()
797
    {
798
        if (self::$recording_level === 0) {
799
            throw new \UnexpectedValueException('Cannot stop recording - already at base level');
800
        }
801
802
        --self::$recording_level;
803
    }
804
805
    /**
806
     * @return array<int, CodeIssue>
0 ignored issues
show
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...
807
     */
808
    public static function clearRecordingLevel()
809
    {
810
        if (self::$recording_level === 0) {
811
            throw new \UnexpectedValueException('Not currently recording');
812
        }
813
814
        $recorded_issues = self::$recorded_issues[self::$recording_level];
815
816
        self::$recorded_issues[self::$recording_level] = [];
817
818
        return $recorded_issues;
819
    }
820
821
    /**
822
     * @return void
823
     */
824
    public static function bubbleUp(CodeIssue $e)
825
    {
826
        if (self::$recording_level === 0) {
827
            self::add($e);
828
829
            return;
830
        }
831
832
        self::$recorded_issues[self::$recording_level][] = $e;
833
    }
834
}
835