Chris@17: Chris@17: * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) Chris@17: * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence Chris@17: */ Chris@17: Chris@17: namespace PHP_CodeSniffer; Chris@17: Chris@17: use PHP_CodeSniffer\Reports\Report; Chris@17: use PHP_CodeSniffer\Files\File; Chris@17: use PHP_CodeSniffer\Exceptions\RuntimeException; Chris@17: use PHP_CodeSniffer\Exceptions\DeepExitException; Chris@17: use PHP_CodeSniffer\Util\Common; Chris@17: Chris@17: class Reporter Chris@17: { Chris@17: Chris@17: /** Chris@17: * The config data for the run. Chris@17: * Chris@17: * @var \PHP_CodeSniffer\Config Chris@17: */ Chris@17: public $config = null; Chris@17: Chris@17: /** Chris@17: * Total number of files that contain errors or warnings. Chris@17: * Chris@17: * @var integer Chris@17: */ Chris@17: public $totalFiles = 0; Chris@17: Chris@17: /** Chris@17: * Total number of errors found during the run. Chris@17: * Chris@17: * @var integer Chris@17: */ Chris@17: public $totalErrors = 0; Chris@17: Chris@17: /** Chris@17: * Total number of warnings found during the run. Chris@17: * Chris@17: * @var integer Chris@17: */ Chris@17: public $totalWarnings = 0; Chris@17: Chris@17: /** Chris@17: * Total number of errors/warnings that can be fixed. Chris@17: * Chris@17: * @var integer Chris@17: */ Chris@17: public $totalFixable = 0; Chris@17: Chris@17: /** Chris@17: * Total number of errors/warnings that were fixed. Chris@17: * Chris@17: * @var integer Chris@17: */ Chris@17: public $totalFixed = 0; Chris@17: Chris@17: /** Chris@17: * When the PHPCS run started. Chris@17: * Chris@17: * @var float Chris@17: */ Chris@17: public static $startTime = 0; Chris@17: Chris@17: /** Chris@17: * A cache of report objects. Chris@17: * Chris@17: * @var array Chris@17: */ Chris@17: private $reports = []; Chris@17: Chris@17: /** Chris@17: * A cache of opened temporary files. Chris@17: * Chris@17: * @var array Chris@17: */ Chris@17: private $tmpFiles = []; Chris@17: Chris@17: Chris@17: /** Chris@17: * Initialise the reporter. Chris@17: * Chris@17: * All reports specified in the config will be created and their Chris@17: * output file (or a temp file if none is specified) initialised by Chris@17: * clearing the current contents. Chris@17: * Chris@17: * @param \PHP_CodeSniffer\Config $config The config data for the run. Chris@17: * Chris@17: * @return void Chris@18: * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If a report is not available. Chris@17: */ Chris@17: public function __construct(Config $config) Chris@17: { Chris@17: $this->config = $config; Chris@17: Chris@17: foreach ($config->reports as $type => $output) { Chris@17: if ($output === null) { Chris@17: $output = $config->reportFile; Chris@17: } Chris@17: Chris@17: $reportClassName = ''; Chris@17: if (strpos($type, '.') !== false) { Chris@17: // This is a path to a custom report class. Chris@17: $filename = realpath($type); Chris@17: if ($filename === false) { Chris@17: $error = "ERROR: Custom report \"$type\" not found".PHP_EOL; Chris@17: throw new DeepExitException($error, 3); Chris@17: } Chris@17: Chris@17: $reportClassName = Autoload::loadFile($filename); Chris@17: } else if (class_exists('PHP_CodeSniffer\Reports\\'.ucfirst($type)) === true) { Chris@17: // PHPCS native report. Chris@17: $reportClassName = 'PHP_CodeSniffer\Reports\\'.ucfirst($type); Chris@17: } else if (class_exists($type) === true) { Chris@17: // FQN of a custom report. Chris@17: $reportClassName = $type; Chris@17: } else { Chris@17: // OK, so not a FQN, try and find the report using the registered namespaces. Chris@17: $registeredNamespaces = Autoload::getSearchPaths(); Chris@17: $trimmedType = ltrim($type, '\\'); Chris@17: Chris@17: foreach ($registeredNamespaces as $nsPrefix) { Chris@17: if ($nsPrefix === '') { Chris@17: continue; Chris@17: } Chris@17: Chris@17: if (class_exists($nsPrefix.'\\'.$trimmedType) === true) { Chris@17: $reportClassName = $nsPrefix.'\\'.$trimmedType; Chris@17: break; Chris@17: } Chris@17: } Chris@17: }//end if Chris@17: Chris@17: if ($reportClassName === '') { Chris@17: $error = "ERROR: Class file for report \"$type\" not found".PHP_EOL; Chris@17: throw new DeepExitException($error, 3); Chris@17: } Chris@17: Chris@17: $reportClass = new $reportClassName(); Chris@17: if (false === ($reportClass instanceof Report)) { Chris@17: throw new RuntimeException('Class "'.$reportClassName.'" must implement the "PHP_CodeSniffer\Report" interface.'); Chris@17: } Chris@17: Chris@17: $this->reports[$type] = [ Chris@17: 'output' => $output, Chris@17: 'class' => $reportClass, Chris@17: ]; Chris@17: Chris@17: if ($output === null) { Chris@17: // Using a temp file. Chris@17: // This needs to be set in the constructor so that all Chris@17: // child procs use the same report file when running in parallel. Chris@17: $this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs'); Chris@17: file_put_contents($this->tmpFiles[$type], ''); Chris@17: } else { Chris@17: file_put_contents($output, ''); Chris@17: } Chris@17: }//end foreach Chris@17: Chris@17: }//end __construct() Chris@17: Chris@17: Chris@17: /** Chris@17: * Generates and prints final versions of all reports. Chris@17: * Chris@17: * Returns TRUE if any of the reports output content to the screen Chris@17: * or FALSE if all reports were silently printed to a file. Chris@17: * Chris@17: * @return bool Chris@17: */ Chris@17: public function printReports() Chris@17: { Chris@17: $toScreen = false; Chris@17: foreach ($this->reports as $type => $report) { Chris@17: if ($report['output'] === null) { Chris@17: $toScreen = true; Chris@17: } Chris@17: Chris@17: $this->printReport($type); Chris@17: } Chris@17: Chris@17: return $toScreen; Chris@17: Chris@17: }//end printReports() Chris@17: Chris@17: Chris@17: /** Chris@17: * Generates and prints a single final report. Chris@17: * Chris@17: * @param string $report The report type to print. Chris@17: * Chris@17: * @return void Chris@17: */ Chris@17: public function printReport($report) Chris@17: { Chris@17: $reportClass = $this->reports[$report]['class']; Chris@17: $reportFile = $this->reports[$report]['output']; Chris@17: Chris@17: if ($reportFile !== null) { Chris@17: $filename = $reportFile; Chris@17: $toScreen = false; Chris@17: } else { Chris@17: if (isset($this->tmpFiles[$report]) === true) { Chris@17: $filename = $this->tmpFiles[$report]; Chris@17: } else { Chris@17: $filename = null; Chris@17: } Chris@17: Chris@17: $toScreen = true; Chris@17: } Chris@17: Chris@17: $reportCache = ''; Chris@17: if ($filename !== null) { Chris@17: $reportCache = file_get_contents($filename); Chris@17: } Chris@17: Chris@17: ob_start(); Chris@17: $reportClass->generate( Chris@17: $reportCache, Chris@17: $this->totalFiles, Chris@17: $this->totalErrors, Chris@17: $this->totalWarnings, Chris@17: $this->totalFixable, Chris@17: $this->config->showSources, Chris@17: $this->config->reportWidth, Chris@17: $this->config->interactive, Chris@17: $toScreen Chris@17: ); Chris@17: $generatedReport = ob_get_contents(); Chris@17: ob_end_clean(); Chris@17: Chris@17: if ($this->config->colors !== true || $reportFile !== null) { Chris@17: $generatedReport = preg_replace('`\033\[[0-9;]+m`', '', $generatedReport); Chris@17: } Chris@17: Chris@17: if ($reportFile !== null) { Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 0) { Chris@17: echo $generatedReport; Chris@17: } Chris@17: Chris@17: file_put_contents($reportFile, $generatedReport.PHP_EOL); Chris@17: } else { Chris@17: echo $generatedReport; Chris@17: if ($filename !== null && file_exists($filename) === true) { Chris@17: unlink($filename); Chris@17: unset($this->tmpFiles[$report]); Chris@17: } Chris@17: } Chris@17: Chris@17: }//end printReport() Chris@17: Chris@17: Chris@17: /** Chris@17: * Caches the result of a single processed file for all reports. Chris@17: * Chris@17: * The report content that is generated is appended to the output file Chris@17: * assigned to each report. This content may be an intermediate report format Chris@17: * and not reflect the final report output. Chris@17: * Chris@17: * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed. Chris@17: * Chris@17: * @return void Chris@17: */ Chris@17: public function cacheFileReport(File $phpcsFile) Chris@17: { Chris@17: if (isset($this->config->reports) === false) { Chris@17: // This happens during unit testing, or any time someone just wants Chris@17: // the error data and not the printed report. Chris@17: return; Chris@17: } Chris@17: Chris@17: $reportData = $this->prepareFileReport($phpcsFile); Chris@17: $errorsShown = false; Chris@17: Chris@17: foreach ($this->reports as $type => $report) { Chris@17: $reportClass = $report['class']; Chris@17: Chris@17: ob_start(); Chris@17: $result = $reportClass->generateFileReport($reportData, $phpcsFile, $this->config->showSources, $this->config->reportWidth); Chris@17: if ($result === true) { Chris@17: $errorsShown = true; Chris@17: } Chris@17: Chris@17: $generatedReport = ob_get_contents(); Chris@17: ob_end_clean(); Chris@17: Chris@17: if ($report['output'] === null) { Chris@17: // Using a temp file. Chris@17: if (isset($this->tmpFiles[$type]) === false) { Chris@17: // When running in interactive mode, the reporter prints the full Chris@17: // report many times, which will unlink the temp file. So we need Chris@17: // to create a new one if it doesn't exist. Chris@17: $this->tmpFiles[$type] = tempnam(sys_get_temp_dir(), 'phpcs'); Chris@17: file_put_contents($this->tmpFiles[$type], ''); Chris@17: } Chris@17: Chris@17: file_put_contents($this->tmpFiles[$type], $generatedReport, (FILE_APPEND | LOCK_EX)); Chris@17: } else { Chris@17: file_put_contents($report['output'], $generatedReport, (FILE_APPEND | LOCK_EX)); Chris@17: }//end if Chris@17: }//end foreach Chris@17: Chris@17: if ($errorsShown === true || PHP_CODESNIFFER_CBF === true) { Chris@17: $this->totalFiles++; Chris@17: $this->totalErrors += $reportData['errors']; Chris@17: $this->totalWarnings += $reportData['warnings']; Chris@17: Chris@17: // When PHPCBF is running, we need to use the fixable error values Chris@17: // after the report has run and fixed what it can. Chris@17: if (PHP_CODESNIFFER_CBF === true) { Chris@17: $this->totalFixable += $phpcsFile->getFixableCount(); Chris@17: $this->totalFixed += $phpcsFile->getFixedCount(); Chris@17: } else { Chris@17: $this->totalFixable += $reportData['fixable']; Chris@17: } Chris@17: } Chris@17: Chris@17: }//end cacheFileReport() Chris@17: Chris@17: Chris@17: /** Chris@17: * Generate summary information to be used during report generation. Chris@17: * Chris@17: * @param \PHP_CodeSniffer\Files\File $phpcsFile The file that has been processed. Chris@17: * Chris@17: * @return array Chris@17: */ Chris@17: public function prepareFileReport(File $phpcsFile) Chris@17: { Chris@17: $report = [ Chris@17: 'filename' => Common::stripBasepath($phpcsFile->getFilename(), $this->config->basepath), Chris@17: 'errors' => $phpcsFile->getErrorCount(), Chris@17: 'warnings' => $phpcsFile->getWarningCount(), Chris@17: 'fixable' => $phpcsFile->getFixableCount(), Chris@17: 'messages' => [], Chris@17: ]; Chris@17: Chris@17: if ($report['errors'] === 0 && $report['warnings'] === 0) { Chris@17: // Prefect score! Chris@17: return $report; Chris@17: } Chris@17: Chris@17: if ($this->config->recordErrors === false) { Chris@17: $message = 'Errors are not being recorded but this report requires error messages. '; Chris@17: $message .= 'This report will not show the correct information.'; Chris@17: $report['messages'][1][1] = [ Chris@17: [ Chris@17: 'message' => $message, Chris@17: 'source' => 'Internal.RecordErrors', Chris@17: 'severity' => 5, Chris@17: 'fixable' => false, Chris@17: 'type' => 'ERROR', Chris@17: ], Chris@17: ]; Chris@17: return $report; Chris@17: } Chris@17: Chris@17: $errors = []; Chris@17: Chris@17: // Merge errors and warnings. Chris@17: foreach ($phpcsFile->getErrors() as $line => $lineErrors) { Chris@17: foreach ($lineErrors as $column => $colErrors) { Chris@17: $newErrors = []; Chris@17: foreach ($colErrors as $data) { Chris@17: $newErrors[] = [ Chris@17: 'message' => $data['message'], Chris@17: 'source' => $data['source'], Chris@17: 'severity' => $data['severity'], Chris@17: 'fixable' => $data['fixable'], Chris@17: 'type' => 'ERROR', Chris@17: ]; Chris@17: } Chris@17: Chris@17: $errors[$line][$column] = $newErrors; Chris@17: } Chris@17: Chris@17: ksort($errors[$line]); Chris@17: }//end foreach Chris@17: Chris@17: foreach ($phpcsFile->getWarnings() as $line => $lineWarnings) { Chris@17: foreach ($lineWarnings as $column => $colWarnings) { Chris@17: $newWarnings = []; Chris@17: foreach ($colWarnings as $data) { Chris@17: $newWarnings[] = [ Chris@17: 'message' => $data['message'], Chris@17: 'source' => $data['source'], Chris@17: 'severity' => $data['severity'], Chris@17: 'fixable' => $data['fixable'], Chris@17: 'type' => 'WARNING', Chris@17: ]; Chris@17: } Chris@17: Chris@17: if (isset($errors[$line]) === false) { Chris@17: $errors[$line] = []; Chris@17: } Chris@17: Chris@17: if (isset($errors[$line][$column]) === true) { Chris@17: $errors[$line][$column] = array_merge( Chris@17: $newWarnings, Chris@17: $errors[$line][$column] Chris@17: ); Chris@17: } else { Chris@17: $errors[$line][$column] = $newWarnings; Chris@17: } Chris@17: }//end foreach Chris@17: Chris@17: ksort($errors[$line]); Chris@17: }//end foreach Chris@17: Chris@17: ksort($errors); Chris@17: $report['messages'] = $errors; Chris@17: return $report; Chris@17: Chris@17: }//end prepareFileReport() Chris@17: Chris@17: Chris@17: }//end class