• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHP_CodeSniffer / 15253296250

26 May 2025 11:55AM UTC coverage: 78.632% (+0.3%) from 78.375%
15253296250

Pull #1105

github

web-flow
Merge d9441d98f into caf806050
Pull Request #1105: Skip tests when 'git' command is not available

19665 of 25009 relevant lines covered (78.63%)

88.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.1
/src/Reporter.php
1
<?php
2
/**
3
 * Manages reporting of errors and warnings.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer;
11

12
use PHP_CodeSniffer\Exceptions\DeepExitException;
13
use PHP_CodeSniffer\Exceptions\RuntimeException;
14
use PHP_CodeSniffer\Files\File;
15
use PHP_CodeSniffer\Reports\Report;
16
use PHP_CodeSniffer\Util\Common;
17
use PHP_CodeSniffer\Util\ExitCode;
18

19
/**
20
 * Manages reporting of errors and warnings.
21
 *
22
 * @property-read int $totalFixable Total number of errors/warnings that can be fixed.
23
 * @property-read int $totalFixed   Total number of errors/warnings that were fixed.
24
 */
25
class Reporter
26
{
27

28
    /**
29
     * The config data for the run.
30
     *
31
     * @var \PHP_CodeSniffer\Config
32
     */
33
    public $config = null;
34

35
    /**
36
     * Total number of files that contain errors or warnings.
37
     *
38
     * @var integer
39
     */
40
    public $totalFiles = 0;
41

42
    /**
43
     * Total number of errors found during the run.
44
     *
45
     * @var integer
46
     */
47
    public $totalErrors = 0;
48

49
    /**
50
     * Total number of warnings found during the run.
51
     *
52
     * @var integer
53
     */
54
    public $totalWarnings = 0;
55

56
    /**
57
     * Total number of errors that can be fixed.
58
     *
59
     * @var integer
60
     */
61
    public $totalFixableErrors = 0;
62

63
    /**
64
     * Total number of warnings that can be fixed.
65
     *
66
     * @var integer
67
     */
68
    public $totalFixableWarnings = 0;
69

70
    /**
71
     * Total number of errors that were fixed.
72
     *
73
     * @var integer
74
     */
75
    public $totalFixedErrors = 0;
76

77
    /**
78
     * Total number of warnings that were fixed.
79
     *
80
     * @var integer
81
     */
82
    public $totalFixedWarnings = 0;
83

84
    /**
85
     * A cache of report objects.
86
     *
87
     * @var array
88
     */
89
    private $reports = [];
90

91
    /**
92
     * A cache of opened temporary files.
93
     *
94
     * @var array
95
     */
96
    private $tmpFiles = [];
97

98

99
    /**
100
     * Initialise the reporter.
101
     *
102
     * All reports specified in the config will be created and their
103
     * output file (or a temp file if none is specified) initialised by
104
     * clearing the current contents.
105
     *
106
     * @param \PHP_CodeSniffer\Config $config The config data for the run.
107
     *
108
     * @return void
109
     * @throws \PHP_CodeSniffer\Exceptions\DeepExitException If a custom report class could not be found.
110
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException  If a report class is incorrectly set up.
111
     */
112
    public function __construct(Config $config)
×
113
    {
114
        $this->config = $config;
×
115

116
        foreach ($config->reports as $type => $output) {
×
117
            if ($output === null) {
×
118
                $output = $config->reportFile;
×
119
            }
120

121
            $reportClassName = '';
×
122
            if (strpos($type, '.') !== false) {
×
123
                // This is a path to a custom report class.
124
                $filename = realpath($type);
×
125
                if ($filename === false) {
×
126
                    $error = "ERROR: Custom report \"$type\" not found".PHP_EOL;
×
127
                    throw new DeepExitException($error, ExitCode::PROCESS_ERROR);
×
128
                }
129

130
                $reportClassName = Autoload::loadFile($filename);
×
131
            } else if (class_exists('PHP_CodeSniffer\Reports\\'.ucfirst($type)) === true) {
×
132
                // PHPCS native report.
133
                $reportClassName = 'PHP_CodeSniffer\Reports\\'.ucfirst($type);
×
134
            } else if (class_exists($type) === true) {
×
135
                // FQN of a custom report.
136
                $reportClassName = $type;
×
137
            } else {
138
                // OK, so not a FQN, try and find the report using the registered namespaces.
139
                $registeredNamespaces = Autoload::getSearchPaths();
×
140
                $trimmedType          = ltrim($type, '\\');
×
141

142
                foreach ($registeredNamespaces as $nsPrefix) {
×
143
                    if ($nsPrefix === '') {
×
144
                        continue;
×
145
                    }
146

147
                    if (class_exists($nsPrefix.'\\'.$trimmedType) === true) {
×
148
                        $reportClassName = $nsPrefix.'\\'.$trimmedType;
×
149
                        break;
×
150
                    }
151
                }
152
            }//end if
153

154
            if ($reportClassName === '') {
×
155
                $error = "ERROR: Class file for report \"$type\" not found".PHP_EOL;
×
156
                throw new DeepExitException($error, ExitCode::PROCESS_ERROR);
×
157
            }
158

159
            $reportClass = new $reportClassName();
×
160
            if (($reportClass instanceof Report) === false) {
×
161
                throw new RuntimeException('Class "'.$reportClassName.'" must implement the "PHP_CodeSniffer\Report" interface.');
×
162
            }
163

164
            $this->reports[$type] = [
×
165
                'output' => $output,
×
166
                'class'  => $reportClass,
×
167
            ];
168

169
            if ($output === null) {
×
170
                // Using a temp file.
171
                // This needs to be set in the constructor so that all
172
                // child procs use the same report file when running in parallel.
173
                $this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs');
×
174
                file_put_contents($this->tmpFiles[$type], '');
×
175
            } else {
176
                file_put_contents($output, '');
×
177
            }
178
        }//end foreach
179

180
    }//end __construct()
181

182

183
    /**
184
     * Check whether a (virtual) property is set.
185
     *
186
     * @param string $name Property name.
187
     *
188
     * @return bool
189
     */
190
    public function __isset($name)
15✔
191
    {
192
        return ($name === 'totalFixable' || $name === 'totalFixed');
15✔
193

194
    }//end __isset()
195

196

197
    /**
198
     * Get the value of an inaccessible property.
199
     *
200
     * The properties supported via this method are both deprecated since PHP_CodeSniffer 4.0.
201
     * - For $totalFixable, use `($reporter->totalFixableErrors + $reporter->totalFixableWarnings)` instead.
202
     * - For $totalFixed, use `($reporter->totalFixedErrors + $reporter->totalFixedWarnings)` instead.
203
     *
204
     * @param string $name The name of the property.
205
     *
206
     * @return int
207
     *
208
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the setting name is invalid.
209
     */
210
    public function __get($name)
15✔
211
    {
212
        if ($name === 'totalFixable') {
15✔
213
            return ($this->totalFixableErrors + $this->totalFixableWarnings);
6✔
214
        }
215

216
        if ($name === 'totalFixed') {
9✔
217
            return ($this->totalFixedErrors + $this->totalFixedWarnings);
6✔
218
        }
219

220
        throw new RuntimeException("ERROR: access requested to unknown property \"Reporter::\${$name}\"");
3✔
221

222
    }//end __get()
223

224

225
    /**
226
     * Setting a dynamic/virtual property on this class is not allowed.
227
     *
228
     * @param string $name  Property name.
229
     * @param mixed  $value Property value.
230
     *
231
     * @return bool
232
     *
233
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
234
     */
235
    public function __set($name, $value)
3✔
236
    {
237
        throw new RuntimeException("ERROR: setting property \"Reporter::\${$name}\" is not allowed");
3✔
238

239
    }//end __set()
240

241

242
    /**
243
     * Unsetting a dynamic/virtual property on this class is not allowed.
244
     *
245
     * @param string $name Property name.
246
     *
247
     * @return bool
248
     *
249
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
250
     */
251
    public function __unset($name)
3✔
252
    {
253
        throw new RuntimeException("ERROR: unsetting property \"Reporter::\${$name}\" is not allowed");
3✔
254

255
    }//end __unset()
256

257

258
    /**
259
     * Generates and prints final versions of all reports.
260
     *
261
     * Returns TRUE if any of the reports output content to the screen
262
     * or FALSE if all reports were silently printed to a file.
263
     *
264
     * @return bool
265
     */
266
    public function printReports()
×
267
    {
268
        $toScreen = false;
×
269
        foreach ($this->reports as $type => $report) {
×
270
            if ($report['output'] === null) {
×
271
                $toScreen = true;
×
272
            }
273

274
            $this->printReport($type);
×
275
        }
276

277
        return $toScreen;
×
278

279
    }//end printReports()
280

281

282
    /**
283
     * Generates and prints a single final report.
284
     *
285
     * @param string $report The report type to print.
286
     *
287
     * @return void
288
     */
289
    public function printReport($report)
×
290
    {
291
        $reportClass = $this->reports[$report]['class'];
×
292
        $reportFile  = $this->reports[$report]['output'];
×
293

294
        if ($reportFile !== null) {
×
295
            $filename = $reportFile;
×
296
            $toScreen = false;
×
297
        } else {
298
            if (isset($this->tmpFiles[$report]) === true) {
×
299
                $filename = $this->tmpFiles[$report];
×
300
            } else {
301
                $filename = null;
×
302
            }
303

304
            $toScreen = true;
×
305
        }
306

307
        $reportCache = '';
×
308
        if ($filename !== null) {
×
309
            $reportCache = file_get_contents($filename);
×
310
        }
311

312
        ob_start();
×
313
        $reportClass->generate(
×
314
            $reportCache,
×
315
            $this->totalFiles,
×
316
            $this->totalErrors,
×
317
            $this->totalWarnings,
×
318
            ($this->totalFixableErrors + $this->totalFixableWarnings),
×
319
            $this->config->showSources,
×
320
            $this->config->reportWidth,
×
321
            $this->config->interactive,
×
322
            $toScreen
×
323
        );
324
        $generatedReport = ob_get_contents();
×
325
        ob_end_clean();
×
326

327
        if ($this->config->colors !== true || $reportFile !== null) {
×
328
            $generatedReport = Common::stripColors($generatedReport);
×
329
        }
330

331
        if ($reportFile !== null) {
×
332
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
333
                echo $generatedReport;
×
334
            }
335

336
            file_put_contents($reportFile, $generatedReport.PHP_EOL);
×
337
        } else {
338
            echo $generatedReport;
×
339
            if ($filename !== null && file_exists($filename) === true) {
×
340
                unlink($filename);
×
341
                unset($this->tmpFiles[$report]);
×
342
            }
343
        }
344

345
    }//end printReport()
346

347

348
    /**
349
     * Caches the result of a single processed file for all reports.
350
     *
351
     * The report content that is generated is appended to the output file
352
     * assigned to each report. This content may be an intermediate report format
353
     * and not reflect the final report output.
354
     *
355
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed.
356
     *
357
     * @return void
358
     */
359
    public function cacheFileReport(File $phpcsFile)
×
360
    {
361
        if (isset($this->config->reports) === false) {
×
362
            // This happens during unit testing, or any time someone just wants
363
            // the error data and not the printed report.
364
            return;
×
365
        }
366

367
        $reportData  = $this->prepareFileReport($phpcsFile);
×
368
        $errorsShown = false;
×
369

370
        foreach ($this->reports as $type => $report) {
×
371
            $reportClass = $report['class'];
×
372

373
            ob_start();
×
374
            $result = $reportClass->generateFileReport($reportData, $phpcsFile, $this->config->showSources, $this->config->reportWidth);
×
375
            if ($result === true) {
×
376
                $errorsShown = true;
×
377
            }
378

379
            $generatedReport = ob_get_contents();
×
380
            ob_end_clean();
×
381

382
            if ($report['output'] === null) {
×
383
                // Using a temp file.
384
                if (isset($this->tmpFiles[$type]) === false) {
×
385
                    // When running in interactive mode, the reporter prints the full
386
                    // report many times, which will unlink the temp file. So we need
387
                    // to create a new one if it doesn't exist.
388
                    $this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs');
×
389
                    file_put_contents($this->tmpFiles[$type], '');
×
390
                }
391

392
                file_put_contents($this->tmpFiles[$type], $generatedReport, (FILE_APPEND | LOCK_EX));
×
393
            } else {
394
                file_put_contents($report['output'], $generatedReport, (FILE_APPEND | LOCK_EX));
×
395
            }//end if
396
        }//end foreach
397

398
        if ($errorsShown === true || PHP_CODESNIFFER_CBF === true) {
×
399
            $this->totalFiles++;
×
400
            $this->totalErrors   += $reportData['errors'];
×
401
            $this->totalWarnings += $reportData['warnings'];
×
402

403
            // When PHPCBF is running, we need to use the fixable error values
404
            // after the report has run and fixed what it can.
405
            $this->totalFixableErrors   += $phpcsFile->getFixableErrorCount();
×
406
            $this->totalFixableWarnings += $phpcsFile->getFixableWarningCount();
×
407
            $this->totalFixedErrors     += $phpcsFile->getFixedErrorCount();
×
408
            $this->totalFixedWarnings   += $phpcsFile->getFixedWarningCount();
×
409
        }
410

411
    }//end cacheFileReport()
412

413

414
    /**
415
     * Generate summary information to be used during report generation.
416
     *
417
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed.
418
     *
419
     * @return array<string, string|int|array> Prepared report data.
420
     *                                         The format of prepared data is as follows:
421
     *                                         ```
422
     *                                         array(
423
     *                                           'filename' => string The name of the current file.
424
     *                                           'errors'   => int    The number of errors seen in the current file.
425
     *                                           'warnings' => int    The number of warnings seen in the current file.
426
     *                                           'fixable'  => int    The number of fixable issues seen in the current file.
427
     *                                           'messages' => array(
428
     *                                             int <Line number> => array(
429
     *                                               int <Column number> => array(
430
     *                                                 int <Message index> => array(
431
     *                                                   'message'  => string The error/warning message.
432
     *                                                   'source'   => string The full error code for the message.
433
     *                                                   'severity' => int    The severity of the message.
434
     *                                                   'fixable'  => bool   Whether this error/warning is auto-fixable.
435
     *                                                   'type'     => string The type of message. Either 'ERROR' or 'WARNING'.
436
     *                                                 )
437
     *                                               )
438
     *                                             )
439
     *                                           )
440
     *                                         )
441
     *                                         ```
442
     */
443
    public function prepareFileReport(File $phpcsFile)
×
444
    {
445
        $report = [
446
            'filename' => Common::stripBasepath($phpcsFile->getFilename(), $this->config->basepath),
×
447
            'errors'   => $phpcsFile->getErrorCount(),
×
448
            'warnings' => $phpcsFile->getWarningCount(),
×
449
            'fixable'  => $phpcsFile->getFixableCount(),
×
450
            'messages' => [],
451
        ];
452

453
        if ($report['errors'] === 0 && $report['warnings'] === 0) {
×
454
            // Perfect score!
455
            return $report;
×
456
        }
457

458
        if ($this->config->recordErrors === false) {
×
459
            $message  = 'Errors are not being recorded but this report requires error messages. ';
×
460
            $message .= 'This report will not show the correct information.';
×
461
            $report['messages'][1][1] = [
×
462
                [
463
                    'message'  => $message,
×
464
                    'source'   => 'Internal.RecordErrors',
×
465
                    'severity' => 5,
×
466
                    'fixable'  => false,
467
                    'type'     => 'ERROR',
×
468
                ],
469
            ];
470
            return $report;
×
471
        }
472

473
        $errors = [];
×
474

475
        // Merge errors and warnings.
476
        foreach ($phpcsFile->getErrors() as $line => $lineErrors) {
×
477
            foreach ($lineErrors as $column => $colErrors) {
×
478
                $newErrors = [];
×
479
                foreach ($colErrors as $data) {
×
480
                    $newErrors[] = [
×
481
                        'message'  => $data['message'],
×
482
                        'source'   => $data['source'],
×
483
                        'severity' => $data['severity'],
×
484
                        'fixable'  => $data['fixable'],
×
485
                        'type'     => 'ERROR',
×
486
                    ];
487
                }
488

489
                $errors[$line][$column] = $newErrors;
×
490
            }
491

492
            ksort($errors[$line]);
×
493
        }//end foreach
494

495
        foreach ($phpcsFile->getWarnings() as $line => $lineWarnings) {
×
496
            foreach ($lineWarnings as $column => $colWarnings) {
×
497
                $newWarnings = [];
×
498
                foreach ($colWarnings as $data) {
×
499
                    $newWarnings[] = [
×
500
                        'message'  => $data['message'],
×
501
                        'source'   => $data['source'],
×
502
                        'severity' => $data['severity'],
×
503
                        'fixable'  => $data['fixable'],
×
504
                        'type'     => 'WARNING',
×
505
                    ];
506
                }
507

508
                if (isset($errors[$line]) === false) {
×
509
                    $errors[$line] = [];
×
510
                }
511

512
                if (isset($errors[$line][$column]) === true) {
×
513
                    $errors[$line][$column] = array_merge(
×
514
                        $newWarnings,
×
515
                        $errors[$line][$column]
×
516
                    );
517
                } else {
518
                    $errors[$line][$column] = $newWarnings;
×
519
                }
520
            }//end foreach
521

522
            ksort($errors[$line]);
×
523
        }//end foreach
524

525
        ksort($errors);
×
526
        $report['messages'] = $errors;
×
527
        return $report;
×
528

529
    }//end prepareFileReport()
530

531

532
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc