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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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)) { |
|
|
|
|
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])) { |
|
|
|
|
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>> |
|
|
|
|
295
|
|
|
*/ |
296
|
|
|
public static function getIssuesData() |
297
|
|
|
{ |
298
|
|
|
return self::$issues_data; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* @return list<IssueData> |
|
|
|
|
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> |
|
|
|
|
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])) { |
|
|
|
|
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>> |
|
|
|
|
333
|
|
|
*/ |
334
|
|
|
public static function getUnusedSuppressions() : array |
335
|
|
|
{ |
336
|
|
|
return self::$unused_suppressions; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* @return array<string, array<int, bool>> |
|
|
|
|
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(); |
|
|
|
|
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>> |
|
|
|
|
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> |
|
|
|
|
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
|
|
|
|
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.