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

PHPCSStandards / PHP_CodeSniffer / 14801268805

02 May 2025 06:41PM UTC coverage: 78.471%. Remained the same
14801268805

Pull #1069

github

web-flow
Merge df3a30715 into 10495a2d0
Pull Request #1069: ProcessRulesetCliArgsTest: add tests for `cache` arg

19566 of 24934 relevant lines covered (78.47%)

86.19 hits per line

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

0.0
/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

18
class Reporter
19
{
20

21
    /**
22
     * The config data for the run.
23
     *
24
     * @var \PHP_CodeSniffer\Config
25
     */
26
    public $config = null;
27

28
    /**
29
     * Total number of files that contain errors or warnings.
30
     *
31
     * @var integer
32
     */
33
    public $totalFiles = 0;
34

35
    /**
36
     * Total number of errors found during the run.
37
     *
38
     * @var integer
39
     */
40
    public $totalErrors = 0;
41

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

49
    /**
50
     * Total number of errors/warnings that can be fixed.
51
     *
52
     * @var integer
53
     */
54
    public $totalFixable = 0;
55

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

63
    /**
64
     * A cache of report objects.
65
     *
66
     * @var array
67
     */
68
    private $reports = [];
69

70
    /**
71
     * A cache of opened temporary files.
72
     *
73
     * @var array
74
     */
75
    private $tmpFiles = [];
76

77

78
    /**
79
     * Initialise the reporter.
80
     *
81
     * All reports specified in the config will be created and their
82
     * output file (or a temp file if none is specified) initialised by
83
     * clearing the current contents.
84
     *
85
     * @param \PHP_CodeSniffer\Config $config The config data for the run.
86
     *
87
     * @return void
88
     * @throws \PHP_CodeSniffer\Exceptions\DeepExitException If a custom report class could not be found.
89
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException  If a report class is incorrectly set up.
90
     */
91
    public function __construct(Config $config)
×
92
    {
93
        $this->config = $config;
×
94

95
        foreach ($config->reports as $type => $output) {
×
96
            if ($output === null) {
×
97
                $output = $config->reportFile;
×
98
            }
99

100
            $reportClassName = '';
×
101
            if (strpos($type, '.') !== false) {
×
102
                // This is a path to a custom report class.
103
                $filename = realpath($type);
×
104
                if ($filename === false) {
×
105
                    $error = "ERROR: Custom report \"$type\" not found".PHP_EOL;
×
106
                    throw new DeepExitException($error, 3);
×
107
                }
108

109
                $reportClassName = Autoload::loadFile($filename);
×
110
            } else if (class_exists('PHP_CodeSniffer\Reports\\'.ucfirst($type)) === true) {
×
111
                // PHPCS native report.
112
                $reportClassName = 'PHP_CodeSniffer\Reports\\'.ucfirst($type);
×
113
            } else if (class_exists($type) === true) {
×
114
                // FQN of a custom report.
115
                $reportClassName = $type;
×
116
            } else {
117
                // OK, so not a FQN, try and find the report using the registered namespaces.
118
                $registeredNamespaces = Autoload::getSearchPaths();
×
119
                $trimmedType          = ltrim($type, '\\');
×
120

121
                foreach ($registeredNamespaces as $nsPrefix) {
×
122
                    if ($nsPrefix === '') {
×
123
                        continue;
×
124
                    }
125

126
                    if (class_exists($nsPrefix.'\\'.$trimmedType) === true) {
×
127
                        $reportClassName = $nsPrefix.'\\'.$trimmedType;
×
128
                        break;
×
129
                    }
130
                }
131
            }//end if
132

133
            if ($reportClassName === '') {
×
134
                $error = "ERROR: Class file for report \"$type\" not found".PHP_EOL;
×
135
                throw new DeepExitException($error, 3);
×
136
            }
137

138
            $reportClass = new $reportClassName();
×
139
            if (($reportClass instanceof Report) === false) {
×
140
                throw new RuntimeException('Class "'.$reportClassName.'" must implement the "PHP_CodeSniffer\Report" interface.');
×
141
            }
142

143
            $this->reports[$type] = [
×
144
                'output' => $output,
×
145
                'class'  => $reportClass,
×
146
            ];
147

148
            if ($output === null) {
×
149
                // Using a temp file.
150
                // This needs to be set in the constructor so that all
151
                // child procs use the same report file when running in parallel.
152
                $this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs');
×
153
                file_put_contents($this->tmpFiles[$type], '');
×
154
            } else {
155
                file_put_contents($output, '');
×
156
            }
157
        }//end foreach
158

159
    }//end __construct()
160

161

162
    /**
163
     * Generates and prints final versions of all reports.
164
     *
165
     * Returns TRUE if any of the reports output content to the screen
166
     * or FALSE if all reports were silently printed to a file.
167
     *
168
     * @return bool
169
     */
170
    public function printReports()
×
171
    {
172
        $toScreen = false;
×
173
        foreach ($this->reports as $type => $report) {
×
174
            if ($report['output'] === null) {
×
175
                $toScreen = true;
×
176
            }
177

178
            $this->printReport($type);
×
179
        }
180

181
        return $toScreen;
×
182

183
    }//end printReports()
184

185

186
    /**
187
     * Generates and prints a single final report.
188
     *
189
     * @param string $report The report type to print.
190
     *
191
     * @return void
192
     */
193
    public function printReport($report)
×
194
    {
195
        $reportClass = $this->reports[$report]['class'];
×
196
        $reportFile  = $this->reports[$report]['output'];
×
197

198
        if ($reportFile !== null) {
×
199
            $filename = $reportFile;
×
200
            $toScreen = false;
×
201
        } else {
202
            if (isset($this->tmpFiles[$report]) === true) {
×
203
                $filename = $this->tmpFiles[$report];
×
204
            } else {
205
                $filename = null;
×
206
            }
207

208
            $toScreen = true;
×
209
        }
210

211
        $reportCache = '';
×
212
        if ($filename !== null) {
×
213
            $reportCache = file_get_contents($filename);
×
214
        }
215

216
        ob_start();
×
217
        $reportClass->generate(
×
218
            $reportCache,
×
219
            $this->totalFiles,
×
220
            $this->totalErrors,
×
221
            $this->totalWarnings,
×
222
            $this->totalFixable,
×
223
            $this->config->showSources,
×
224
            $this->config->reportWidth,
×
225
            $this->config->interactive,
×
226
            $toScreen
×
227
        );
228
        $generatedReport = ob_get_contents();
×
229
        ob_end_clean();
×
230

231
        if ($this->config->colors !== true || $reportFile !== null) {
×
232
            $generatedReport = Common::stripColors($generatedReport);
×
233
        }
234

235
        if ($reportFile !== null) {
×
236
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
237
                echo $generatedReport;
×
238
            }
239

240
            file_put_contents($reportFile, $generatedReport.PHP_EOL);
×
241
        } else {
242
            echo $generatedReport;
×
243
            if ($filename !== null && file_exists($filename) === true) {
×
244
                unlink($filename);
×
245
                unset($this->tmpFiles[$report]);
×
246
            }
247
        }
248

249
    }//end printReport()
250

251

252
    /**
253
     * Caches the result of a single processed file for all reports.
254
     *
255
     * The report content that is generated is appended to the output file
256
     * assigned to each report. This content may be an intermediate report format
257
     * and not reflect the final report output.
258
     *
259
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed.
260
     *
261
     * @return void
262
     */
263
    public function cacheFileReport(File $phpcsFile)
×
264
    {
265
        if (isset($this->config->reports) === false) {
×
266
            // This happens during unit testing, or any time someone just wants
267
            // the error data and not the printed report.
268
            return;
×
269
        }
270

271
        $reportData  = $this->prepareFileReport($phpcsFile);
×
272
        $errorsShown = false;
×
273

274
        foreach ($this->reports as $type => $report) {
×
275
            $reportClass = $report['class'];
×
276

277
            ob_start();
×
278
            $result = $reportClass->generateFileReport($reportData, $phpcsFile, $this->config->showSources, $this->config->reportWidth);
×
279
            if ($result === true) {
×
280
                $errorsShown = true;
×
281
            }
282

283
            $generatedReport = ob_get_contents();
×
284
            ob_end_clean();
×
285

286
            if ($report['output'] === null) {
×
287
                // Using a temp file.
288
                if (isset($this->tmpFiles[$type]) === false) {
×
289
                    // When running in interactive mode, the reporter prints the full
290
                    // report many times, which will unlink the temp file. So we need
291
                    // to create a new one if it doesn't exist.
292
                    $this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs');
×
293
                    file_put_contents($this->tmpFiles[$type], '');
×
294
                }
295

296
                file_put_contents($this->tmpFiles[$type], $generatedReport, (FILE_APPEND | LOCK_EX));
×
297
            } else {
298
                file_put_contents($report['output'], $generatedReport, (FILE_APPEND | LOCK_EX));
×
299
            }//end if
300
        }//end foreach
301

302
        if ($errorsShown === true || PHP_CODESNIFFER_CBF === true) {
×
303
            $this->totalFiles++;
×
304
            $this->totalErrors   += $reportData['errors'];
×
305
            $this->totalWarnings += $reportData['warnings'];
×
306

307
            // When PHPCBF is running, we need to use the fixable error values
308
            // after the report has run and fixed what it can.
309
            if (PHP_CODESNIFFER_CBF === true) {
×
310
                $this->totalFixable += $phpcsFile->getFixableCount();
×
311
                $this->totalFixed   += $phpcsFile->getFixedCount();
×
312
            } else {
313
                $this->totalFixable += $reportData['fixable'];
×
314
            }
315
        }
316

317
    }//end cacheFileReport()
318

319

320
    /**
321
     * Generate summary information to be used during report generation.
322
     *
323
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed.
324
     *
325
     * @return array<string, string|int|array> Prepared report data.
326
     *                                         The format of prepared data is as follows:
327
     *                                         ```
328
     *                                         array(
329
     *                                           'filename' => string The name of the current file.
330
     *                                           'errors'   => int    The number of errors seen in the current file.
331
     *                                           'warnings' => int    The number of warnings seen in the current file.
332
     *                                           'fixable'  => int    The number of fixable issues seen in the current file.
333
     *                                           'messages' => array(
334
     *                                             int <Line number> => array(
335
     *                                               int <Column number> => array(
336
     *                                                 int <Message index> => array(
337
     *                                                   'message'  => string The error/warning message.
338
     *                                                   'source'   => string The full error code for the message.
339
     *                                                   'severity' => int    The severity of the message.
340
     *                                                   'fixable'  => bool   Whether this error/warning is auto-fixable.
341
     *                                                   'type'     => string The type of message. Either 'ERROR' or 'WARNING'.
342
     *                                                 )
343
     *                                               )
344
     *                                             )
345
     *                                           )
346
     *                                         )
347
     *                                         ```
348
     */
349
    public function prepareFileReport(File $phpcsFile)
×
350
    {
351
        $report = [
352
            'filename' => Common::stripBasepath($phpcsFile->getFilename(), $this->config->basepath),
×
353
            'errors'   => $phpcsFile->getErrorCount(),
×
354
            'warnings' => $phpcsFile->getWarningCount(),
×
355
            'fixable'  => $phpcsFile->getFixableCount(),
×
356
            'messages' => [],
357
        ];
358

359
        if ($report['errors'] === 0 && $report['warnings'] === 0) {
×
360
            // Perfect score!
361
            return $report;
×
362
        }
363

364
        if ($this->config->recordErrors === false) {
×
365
            $message  = 'Errors are not being recorded but this report requires error messages. ';
×
366
            $message .= 'This report will not show the correct information.';
×
367
            $report['messages'][1][1] = [
×
368
                [
369
                    'message'  => $message,
×
370
                    'source'   => 'Internal.RecordErrors',
×
371
                    'severity' => 5,
×
372
                    'fixable'  => false,
373
                    'type'     => 'ERROR',
×
374
                ],
375
            ];
376
            return $report;
×
377
        }
378

379
        $errors = [];
×
380

381
        // Merge errors and warnings.
382
        foreach ($phpcsFile->getErrors() as $line => $lineErrors) {
×
383
            foreach ($lineErrors as $column => $colErrors) {
×
384
                $newErrors = [];
×
385
                foreach ($colErrors as $data) {
×
386
                    $newErrors[] = [
×
387
                        'message'  => $data['message'],
×
388
                        'source'   => $data['source'],
×
389
                        'severity' => $data['severity'],
×
390
                        'fixable'  => $data['fixable'],
×
391
                        'type'     => 'ERROR',
×
392
                    ];
393
                }
394

395
                $errors[$line][$column] = $newErrors;
×
396
            }
397

398
            ksort($errors[$line]);
×
399
        }//end foreach
400

401
        foreach ($phpcsFile->getWarnings() as $line => $lineWarnings) {
×
402
            foreach ($lineWarnings as $column => $colWarnings) {
×
403
                $newWarnings = [];
×
404
                foreach ($colWarnings as $data) {
×
405
                    $newWarnings[] = [
×
406
                        'message'  => $data['message'],
×
407
                        'source'   => $data['source'],
×
408
                        'severity' => $data['severity'],
×
409
                        'fixable'  => $data['fixable'],
×
410
                        'type'     => 'WARNING',
×
411
                    ];
412
                }
413

414
                if (isset($errors[$line]) === false) {
×
415
                    $errors[$line] = [];
×
416
                }
417

418
                if (isset($errors[$line][$column]) === true) {
×
419
                    $errors[$line][$column] = array_merge(
×
420
                        $newWarnings,
×
421
                        $errors[$line][$column]
×
422
                    );
423
                } else {
424
                    $errors[$line][$column] = $newWarnings;
×
425
                }
426
            }//end foreach
427

428
            ksort($errors[$line]);
×
429
        }//end foreach
430

431
        ksort($errors);
×
432
        $report['messages'] = $errors;
×
433
        return $report;
×
434

435
    }//end prepareFileReport()
436

437

438
}//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

© 2026 Coveralls, Inc