Reporter.php 13 KB

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