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/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 8 */ 9 10namespace PHP_CodeSniffer; 11 12use PHP_CodeSniffer\Exceptions\DeepExitException; 13use PHP_CodeSniffer\Exceptions\RuntimeException; 14use PHP_CodeSniffer\Files\File; 15use PHP_CodeSniffer\Reports\Report; 16use PHP_CodeSniffer\Util\Common; 17 18class 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 * When the PHPCS run started. 65 * 66 * @var float 67 */ 68 public static $startTime = 0; 69 70 /** 71 * A cache of report objects. 72 * 73 * @var array 74 */ 75 private $reports = []; 76 77 /** 78 * A cache of opened temporary files. 79 * 80 * @var array 81 */ 82 private $tmpFiles = []; 83 84 85 /** 86 * Initialise the reporter. 87 * 88 * All reports specified in the config will be created and their 89 * output file (or a temp file if none is specified) initialised by 90 * clearing the current contents. 91 * 92 * @param \PHP_CodeSniffer\Config $config The config data for the run. 93 * 94 * @return void 95 * @throws \PHP_CodeSniffer\Exceptions\DeepExitException If a custom report class could not be found. 96 * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If a report class is incorrectly set up. 97 */ 98 public function __construct(Config $config) 99 { 100 $this->config = $config; 101 102 foreach ($config->reports as $type => $output) { 103 if ($output === null) { 104 $output = $config->reportFile; 105 } 106 107 $reportClassName = ''; 108 if (strpos($type, '.') !== false) { 109 // This is a path to a custom report class. 110 $filename = realpath($type); 111 if ($filename === false) { 112 $error = "ERROR: Custom report \"$type\" not found".PHP_EOL; 113 throw new DeepExitException($error, 3); 114 } 115 116 $reportClassName = Autoload::loadFile($filename); 117 } else if (class_exists('PHP_CodeSniffer\Reports\\'.ucfirst($type)) === true) { 118 // PHPCS native report. 119 $reportClassName = 'PHP_CodeSniffer\Reports\\'.ucfirst($type); 120 } else if (class_exists($type) === true) { 121 // FQN of a custom report. 122 $reportClassName = $type; 123 } else { 124 // OK, so not a FQN, try and find the report using the registered namespaces. 125 $registeredNamespaces = Autoload::getSearchPaths(); 126 $trimmedType = ltrim($type, '\\'); 127 128 foreach ($registeredNamespaces as $nsPrefix) { 129 if ($nsPrefix === '') { 130 continue; 131 } 132 133 if (class_exists($nsPrefix.'\\'.$trimmedType) === true) { 134 $reportClassName = $nsPrefix.'\\'.$trimmedType; 135 break; 136 } 137 } 138 }//end if 139 140 if ($reportClassName === '') { 141 $error = "ERROR: Class file for report \"$type\" not found".PHP_EOL; 142 throw new DeepExitException($error, 3); 143 } 144 145 $reportClass = new $reportClassName(); 146 if (($reportClass instanceof Report) === false) { 147 throw new RuntimeException('Class "'.$reportClassName.'" must implement the "PHP_CodeSniffer\Report" interface.'); 148 } 149 150 $this->reports[$type] = [ 151 'output' => $output, 152 'class' => $reportClass, 153 ]; 154 155 if ($output === null) { 156 // Using a temp file. 157 // This needs to be set in the constructor so that all 158 // child procs use the same report file when running in parallel. 159 $this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs'); 160 file_put_contents($this->tmpFiles[$type], ''); 161 } else { 162 file_put_contents($output, ''); 163 } 164 }//end foreach 165 166 }//end __construct() 167 168 169 /** 170 * Generates and prints final versions of all reports. 171 * 172 * Returns TRUE if any of the reports output content to the screen 173 * or FALSE if all reports were silently printed to a file. 174 * 175 * @return bool 176 */ 177 public function printReports() 178 { 179 $toScreen = false; 180 foreach ($this->reports as $type => $report) { 181 if ($report['output'] === null) { 182 $toScreen = true; 183 } 184 185 $this->printReport($type); 186 } 187 188 return $toScreen; 189 190 }//end printReports() 191 192 193 /** 194 * Generates and prints a single final report. 195 * 196 * @param string $report The report type to print. 197 * 198 * @return void 199 */ 200 public function printReport($report) 201 { 202 $reportClass = $this->reports[$report]['class']; 203 $reportFile = $this->reports[$report]['output']; 204 205 if ($reportFile !== null) { 206 $filename = $reportFile; 207 $toScreen = false; 208 } else { 209 if (isset($this->tmpFiles[$report]) === true) { 210 $filename = $this->tmpFiles[$report]; 211 } else { 212 $filename = null; 213 } 214 215 $toScreen = true; 216 } 217 218 $reportCache = ''; 219 if ($filename !== null) { 220 $reportCache = file_get_contents($filename); 221 } 222 223 ob_start(); 224 $reportClass->generate( 225 $reportCache, 226 $this->totalFiles, 227 $this->totalErrors, 228 $this->totalWarnings, 229 $this->totalFixable, 230 $this->config->showSources, 231 $this->config->reportWidth, 232 $this->config->interactive, 233 $toScreen 234 ); 235 $generatedReport = ob_get_contents(); 236 ob_end_clean(); 237 238 if ($this->config->colors !== true || $reportFile !== null) { 239 $generatedReport = preg_replace('`\033\[[0-9;]+m`', '', $generatedReport); 240 } 241 242 if ($reportFile !== null) { 243 if (PHP_CODESNIFFER_VERBOSITY > 0) { 244 echo $generatedReport; 245 } 246 247 file_put_contents($reportFile, $generatedReport.PHP_EOL); 248 } else { 249 echo $generatedReport; 250 if ($filename !== null && file_exists($filename) === true) { 251 unlink($filename); 252 unset($this->tmpFiles[$report]); 253 } 254 } 255 256 }//end printReport() 257 258 259 /** 260 * Caches the result of a single processed file for all reports. 261 * 262 * The report content that is generated is appended to the output file 263 * assigned to each report. This content may be an intermediate report format 264 * and not reflect the final report output. 265 * 266 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed. 267 * 268 * @return void 269 */ 270 public function cacheFileReport(File $phpcsFile) 271 { 272 if (isset($this->config->reports) === false) { 273 // This happens during unit testing, or any time someone just wants 274 // the error data and not the printed report. 275 return; 276 } 277 278 $reportData = $this->prepareFileReport($phpcsFile); 279 $errorsShown = false; 280 281 foreach ($this->reports as $type => $report) { 282 $reportClass = $report['class']; 283 284 ob_start(); 285 $result = $reportClass->generateFileReport($reportData, $phpcsFile, $this->config->showSources, $this->config->reportWidth); 286 if ($result === true) { 287 $errorsShown = true; 288 } 289 290 $generatedReport = ob_get_contents(); 291 ob_end_clean(); 292 293 if ($report['output'] === null) { 294 // Using a temp file. 295 if (isset($this->tmpFiles[$type]) === false) { 296 // When running in interactive mode, the reporter prints the full 297 // report many times, which will unlink the temp file. So we need 298 // to create a new one if it doesn't exist. 299 $this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs'); 300 file_put_contents($this->tmpFiles[$type], ''); 301 } 302 303 file_put_contents($this->tmpFiles[$type], $generatedReport, (FILE_APPEND | LOCK_EX)); 304 } else { 305 file_put_contents($report['output'], $generatedReport, (FILE_APPEND | LOCK_EX)); 306 }//end if 307 }//end foreach 308 309 if ($errorsShown === true || PHP_CODESNIFFER_CBF === true) { 310 $this->totalFiles++; 311 $this->totalErrors += $reportData['errors']; 312 $this->totalWarnings += $reportData['warnings']; 313 314 // When PHPCBF is running, we need to use the fixable error values 315 // after the report has run and fixed what it can. 316 if (PHP_CODESNIFFER_CBF === true) { 317 $this->totalFixable += $phpcsFile->getFixableCount(); 318 $this->totalFixed += $phpcsFile->getFixedCount(); 319 } else { 320 $this->totalFixable += $reportData['fixable']; 321 } 322 } 323 324 }//end cacheFileReport() 325 326 327 /** 328 * Generate summary information to be used during report generation. 329 * 330 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed. 331 * 332 * @return array 333 */ 334 public function prepareFileReport(File $phpcsFile) 335 { 336 $report = [ 337 'filename' => Common::stripBasepath($phpcsFile->getFilename(), $this->config->basepath), 338 'errors' => $phpcsFile->getErrorCount(), 339 'warnings' => $phpcsFile->getWarningCount(), 340 'fixable' => $phpcsFile->getFixableCount(), 341 'messages' => [], 342 ]; 343 344 if ($report['errors'] === 0 && $report['warnings'] === 0) { 345 // Prefect score! 346 return $report; 347 } 348 349 if ($this->config->recordErrors === false) { 350 $message = 'Errors are not being recorded but this report requires error messages. '; 351 $message .= 'This report will not show the correct information.'; 352 $report['messages'][1][1] = [ 353 [ 354 'message' => $message, 355 'source' => 'Internal.RecordErrors', 356 'severity' => 5, 357 'fixable' => false, 358 'type' => 'ERROR', 359 ], 360 ]; 361 return $report; 362 } 363 364 $errors = []; 365 366 // Merge errors and warnings. 367 foreach ($phpcsFile->getErrors() as $line => $lineErrors) { 368 foreach ($lineErrors as $column => $colErrors) { 369 $newErrors = []; 370 foreach ($colErrors as $data) { 371 $newErrors[] = [ 372 'message' => $data['message'], 373 'source' => $data['source'], 374 'severity' => $data['severity'], 375 'fixable' => $data['fixable'], 376 'type' => 'ERROR', 377 ]; 378 } 379 380 $errors[$line][$column] = $newErrors; 381 } 382 383 ksort($errors[$line]); 384 }//end foreach 385 386 foreach ($phpcsFile->getWarnings() as $line => $lineWarnings) { 387 foreach ($lineWarnings as $column => $colWarnings) { 388 $newWarnings = []; 389 foreach ($colWarnings as $data) { 390 $newWarnings[] = [ 391 'message' => $data['message'], 392 'source' => $data['source'], 393 'severity' => $data['severity'], 394 'fixable' => $data['fixable'], 395 'type' => 'WARNING', 396 ]; 397 } 398 399 if (isset($errors[$line]) === false) { 400 $errors[$line] = []; 401 } 402 403 if (isset($errors[$line][$column]) === true) { 404 $errors[$line][$column] = array_merge( 405 $newWarnings, 406 $errors[$line][$column] 407 ); 408 } else { 409 $errors[$line][$column] = $newWarnings; 410 } 411 }//end foreach 412 413 ksort($errors[$line]); 414 }//end foreach 415 416 ksort($errors); 417 $report['messages'] = $errors; 418 return $report; 419 420 }//end prepareFileReport() 421 422 423}//end class 424