PhpCodeSnifferTask.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. <?php
  2. /*
  3. * $Id: PhpCodeSnifferTask.php 905 2010-10-05 16:28:03Z mrook $
  4. *
  5. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  6. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  7. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  8. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  9. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  10. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  11. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  12. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  13. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  14. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  15. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  16. *
  17. * This software consists of voluntary contributions made by many individuals
  18. * and is licensed under the LGPL. For more information please see
  19. * <http://phing.info>.
  20. */
  21. require_once 'phing/Task.php';
  22. /**
  23. * A PHP code sniffer task. Checking the style of one or more PHP source files.
  24. *
  25. * @author Dirk Thomas <dirk.thomas@4wdmedia.de>
  26. * @version $Id: PhpCodeSnifferTask.php 905 2010-10-05 16:28:03Z mrook $
  27. * @package phing.tasks.ext
  28. */
  29. class PhpCodeSnifferTask extends Task {
  30. protected $file; // the source file (from xml attribute)
  31. protected $filesets = array(); // all fileset objects assigned to this task
  32. // parameters for php code sniffer
  33. protected $standard = 'Generic';
  34. protected $sniffs = array();
  35. protected $showWarnings = true;
  36. protected $showSources = false;
  37. protected $reportWidth = 80;
  38. protected $verbosity = 0;
  39. protected $tabWidth = 0;
  40. protected $allowedFileExtensions = array('php');
  41. protected $ignorePatterns = false;
  42. protected $noSubdirectories = false;
  43. protected $configData = array();
  44. // parameters to customize output
  45. protected $showSniffs = false;
  46. protected $format = 'default';
  47. protected $formatters = array();
  48. /**
  49. * Holds the type of the doc generator
  50. *
  51. * @var string
  52. */
  53. protected $docGenerator = '';
  54. /**
  55. * Holds the outfile for the documentation
  56. *
  57. * @var PhingFile
  58. */
  59. protected $docFile = null;
  60. private $haltonerror = false;
  61. private $haltonwarning = false;
  62. /**
  63. * Load the necessary environment for running PHP_CodeSniffer.
  64. *
  65. * @throws BuildException
  66. * @return void
  67. */
  68. public function init()
  69. {
  70. /**
  71. * Determine PHP_CodeSniffer version number
  72. */
  73. preg_match('/\d\.\d\.\d/', shell_exec('phpcs --version'), $version);
  74. if (version_compare($version[0], '1.2.2') < 0) {
  75. throw new BuildException(
  76. 'PhpCodeSnifferTask requires PHP_CodeSniffer version >= 1.2.2',
  77. $this->getLocation()
  78. );
  79. }
  80. }
  81. /**
  82. * File to be performed syntax check on
  83. * @param PhingFile $file
  84. */
  85. public function setFile(PhingFile $file) {
  86. $this->file = $file;
  87. }
  88. /**
  89. * Nested creator, creates a FileSet for this task
  90. *
  91. * @return FileSet The created fileset object
  92. */
  93. function createFileSet() {
  94. $num = array_push($this->filesets, new FileSet());
  95. return $this->filesets[$num-1];
  96. }
  97. /**
  98. * Sets the coding standard to test for
  99. *
  100. * @param string $standard The coding standard
  101. *
  102. * @return void
  103. */
  104. public function setStandard($standard)
  105. {
  106. if (!class_exists('PHP_CodeSniffer')) {
  107. include_once 'PHP/CodeSniffer.php';
  108. }
  109. if (PHP_CodeSniffer::isInstalledStandard($standard) === false) {
  110. // They didn't select a valid coding standard, so help them
  111. // out by letting them know which standards are installed.
  112. $installedStandards = PHP_CodeSniffer::getInstalledStandards();
  113. $numStandards = count($installedStandards);
  114. $errMsg = '';
  115. if ($numStandards === 0) {
  116. $errMsg = 'No coding standards are installed.';
  117. } else {
  118. $lastStandard = array_pop($installedStandards);
  119. if ($numStandards === 1) {
  120. $errMsg = 'The only coding standard installed is ' . $lastStandard;
  121. } else {
  122. $standardList = implode(', ', $installedStandards);
  123. $standardList .= ' and ' . $lastStandard;
  124. $errMsg = 'The installed coding standards are ' . $standardList;
  125. }
  126. }
  127. throw new BuildException(
  128. 'ERROR: the "' . $standard . '" coding standard is not installed. ' . $errMsg,
  129. $this->getLocation()
  130. );
  131. }
  132. $this->standard = $standard;
  133. }
  134. /**
  135. * Sets the sniffs which the standard should be restricted to
  136. * @param string $sniffs
  137. */
  138. public function setSniffs($sniffs)
  139. {
  140. $token = ' ,;';
  141. $sniff = strtok($sniffs, $token);
  142. while ($sniff !== false) {
  143. $this->sniffs[] = $sniff;
  144. $sniff = strtok($token);
  145. }
  146. }
  147. /**
  148. * Sets the type of the doc generator
  149. *
  150. * @param string $generator HTML or Text
  151. *
  152. * @return void
  153. */
  154. public function setDocGenerator($generator)
  155. {
  156. $this->docGenerator = $generator;
  157. }
  158. /**
  159. * Sets the outfile for the documentation
  160. *
  161. * @param PhingFile $file The outfile for the doc
  162. *
  163. * @return void
  164. */
  165. public function setDocFile(PhingFile $file)
  166. {
  167. $this->docFile = $file;
  168. }
  169. /**
  170. * Sets the flag if warnings should be shown
  171. * @param boolean $show
  172. */
  173. public function setShowWarnings($show)
  174. {
  175. $this->showWarnings = StringHelper::booleanValue($show);
  176. }
  177. /**
  178. * Sets the flag if sources should be shown
  179. *
  180. * @param boolean $show Whether to show sources or not
  181. *
  182. * @return void
  183. */
  184. public function setShowSources($show)
  185. {
  186. $this->showSources = StringHelper::booleanValue($show);
  187. }
  188. /**
  189. * Sets the width of the report
  190. *
  191. * @param int $width How wide the screen reports should be.
  192. *
  193. * @return void
  194. */
  195. public function setReportWidth($width)
  196. {
  197. $this->reportWidth = (int) $width;
  198. }
  199. /**
  200. * Sets the verbosity level
  201. * @param int $level
  202. */
  203. public function setVerbosity($level)
  204. {
  205. $this->verbosity = (int)$level;
  206. }
  207. /**
  208. * Sets the tab width to replace tabs with spaces
  209. * @param int $width
  210. */
  211. public function setTabWidth($width)
  212. {
  213. $this->tabWidth = (int)$width;
  214. }
  215. /**
  216. * Sets the allowed file extensions when using directories instead of specific files
  217. * @param array $extensions
  218. */
  219. public function setAllowedFileExtensions($extensions)
  220. {
  221. $this->allowedFileExtensions = array();
  222. $token = ' ,;';
  223. $ext = strtok($extensions, $token);
  224. while ($ext !== false) {
  225. $this->allowedFileExtensions[] = $ext;
  226. $ext = strtok($token);
  227. }
  228. }
  229. /**
  230. * Sets the ignore patterns to skip files when using directories instead of specific files
  231. * @param array $extensions
  232. */
  233. public function setIgnorePatterns($patterns)
  234. {
  235. $this->ignorePatterns = array();
  236. $token = ' ,;';
  237. $pattern = strtok($patterns, $token);
  238. while ($pattern !== false) {
  239. $this->ignorePatterns[] = $pattern;
  240. $pattern = strtok($token);
  241. }
  242. }
  243. /**
  244. * Sets the flag if subdirectories should be skipped
  245. * @param boolean $subdirectories
  246. */
  247. public function setNoSubdirectories($subdirectories)
  248. {
  249. $this->noSubdirectories = StringHelper::booleanValue($subdirectories);
  250. }
  251. /**
  252. * Creates a config parameter for this task
  253. *
  254. * @return Parameter The created parameter
  255. */
  256. public function createConfig() {
  257. $num = array_push($this->configData, new Parameter());
  258. return $this->configData[$num-1];
  259. }
  260. /**
  261. * Sets the flag if the used sniffs should be listed
  262. * @param boolean $show
  263. */
  264. public function setShowSniffs($show)
  265. {
  266. $this->showSniffs = StringHelper::booleanValue($show);
  267. }
  268. /**
  269. * Sets the output format
  270. * @param string $format
  271. */
  272. public function setFormat($format)
  273. {
  274. $this->format = $format;
  275. }
  276. /**
  277. * Create object for nested formatter element.
  278. * @return CodeSniffer_FormatterElement
  279. */
  280. public function createFormatter () {
  281. $num = array_push($this->formatters,
  282. new PhpCodeSnifferTask_FormatterElement());
  283. return $this->formatters[$num-1];
  284. }
  285. /**
  286. * Sets the haltonerror flag
  287. * @param boolean $value
  288. */
  289. function setHaltonerror($value)
  290. {
  291. $this->haltonerror = $value;
  292. }
  293. /**
  294. * Sets the haltonwarning flag
  295. * @param boolean $value
  296. */
  297. function setHaltonwarning($value)
  298. {
  299. $this->haltonwarning = $value;
  300. }
  301. /**
  302. * Executes PHP code sniffer against PhingFile or a FileSet
  303. */
  304. public function main() {
  305. if (!class_exists('PHP_CodeSniffer')) {
  306. include_once 'PHP/CodeSniffer.php';
  307. }
  308. if(!isset($this->file) and count($this->filesets) == 0) {
  309. throw new BuildException("Missing either a nested fileset or attribute 'file' set");
  310. }
  311. if (count($this->formatters) == 0) {
  312. // turn legacy format attribute into formatter
  313. $fmt = new PhpCodeSnifferTask_FormatterElement();
  314. $fmt->setType($this->format);
  315. $fmt->setUseFile(false);
  316. $this->formatters[] = $fmt;
  317. }
  318. if (!isset($this->file))
  319. {
  320. $fileList = array();
  321. $project = $this->getProject();
  322. foreach ($this->filesets as $fs) {
  323. $ds = $fs->getDirectoryScanner($project);
  324. $files = $ds->getIncludedFiles();
  325. $dir = $fs->getDir($this->project)->getAbsolutePath();
  326. foreach ($files as $file) {
  327. $fileList[] = $dir.DIRECTORY_SEPARATOR.$file;
  328. }
  329. }
  330. }
  331. $codeSniffer = new PHP_CodeSniffer($this->verbosity, $this->tabWidth);
  332. $codeSniffer->setAllowedFileExtensions($this->allowedFileExtensions);
  333. if (is_array($this->ignorePatterns)) $codeSniffer->setIgnorePatterns($this->ignorePatterns);
  334. foreach ($this->configData as $configData) {
  335. $codeSniffer->setConfigData($configData->getName(), $configData->getValue(), true);
  336. }
  337. if ($this->file instanceof PhingFile) {
  338. $codeSniffer->process($this->file->getPath(), $this->standard, $this->sniffs, $this->noSubdirectories);
  339. } else {
  340. $codeSniffer->process($fileList, $this->standard, $this->sniffs, $this->noSubdirectories);
  341. }
  342. $report = $this->printErrorReport($codeSniffer);
  343. // generate the documentation
  344. if ($this->docGenerator !== '' && $this->docFile !== null) {
  345. ob_start();
  346. $codeSniffer->generateDocs($this->standard, $this->sniffs, $this->docGenerator);
  347. $output = ob_get_contents();
  348. ob_end_clean();
  349. // write to file
  350. $outputFile = $this->docFile->getPath();
  351. $check = file_put_contents($outputFile, $output);
  352. if (is_bool($check) && !$check) {
  353. throw new BuildException('Error writing doc to ' . $outputFile);
  354. }
  355. } elseif ($this->docGenerator !== '' && $this->docFile === null) {
  356. $codeSniffer->generateDocs($this->standard, $this->sniffs, $this->docGenerator);
  357. }
  358. if ($this->haltonerror && $report['totals']['errors'] > 0)
  359. {
  360. throw new BuildException('phpcodesniffer detected ' . $report['totals']['errors']. ' error' . ($report['totals']['errors'] > 1 ? 's' : ''));
  361. }
  362. if ($this->haltonwarning && $report['totals']['warnings'] > 0)
  363. {
  364. throw new BuildException('phpcodesniffer detected ' . $report['totals']['warnings'] . ' warning' . ($report['totals']['warnings'] > 1 ? 's' : ''));
  365. }
  366. }
  367. /**
  368. * Prints the error report.
  369. *
  370. * @param PHP_CodeSniffer $phpcs The PHP_CodeSniffer object containing
  371. * the errors.
  372. *
  373. * @return int The number of error and warning messages shown.
  374. */
  375. protected function printErrorReport($phpcs)
  376. {
  377. if ($this->showSniffs) {
  378. $sniffs = $phpcs->getSniffs();
  379. $sniffStr = '';
  380. foreach ($sniffs as $sniff) {
  381. $sniffStr .= '- ' . $sniff.PHP_EOL;
  382. }
  383. $this->log('The list of used sniffs (#' . count($sniffs) . '): ' . PHP_EOL . $sniffStr, Project::MSG_INFO);
  384. }
  385. $filesViolations = $phpcs->getFilesErrors();
  386. $reporting = new PHP_CodeSniffer_Reporting();
  387. $report = $reporting->prepare($filesViolations, $this->showWarnings);
  388. // process output
  389. foreach ($this->formatters as $fe) {
  390. switch ($fe->getType()) {
  391. case 'default':
  392. // default format goes to logs, no buffering
  393. $this->outputCustomFormat($report);
  394. $fe->setUseFile(false);
  395. break;
  396. default:
  397. $reportFile = '';
  398. if ($fe->getUseFile()) {
  399. $reportFile = $fe->getOutfile()->getPath();
  400. ob_start();
  401. }
  402. $reporting->printReport(
  403. $fe->getType(),
  404. $filesViolations,
  405. $this->showWarnings,
  406. $this->showSources,
  407. $reportFile,
  408. $this->reportWidth
  409. );
  410. // reporting class uses ob_end_flush(), but we don't want
  411. // an output if we use a file
  412. if ($fe->getUseFile()) {
  413. ob_end_clean();
  414. }
  415. break;
  416. }
  417. }
  418. return $report;
  419. }
  420. /**
  421. * Outputs the results with a custom format
  422. *
  423. * @param array $report Packaged list of all errors in each file
  424. */
  425. protected function outputCustomFormat($report) {
  426. $files = $report['files'];
  427. foreach ($files as $file => $attributes) {
  428. $errors = $attributes['errors'];
  429. $warnings = $attributes['warnings'];
  430. $messages = $attributes['messages'];
  431. if ($errors > 0) {
  432. $this->log($file . ': ' . $errors . ' error' . ($errors > 1 ? 's' : '') . ' detected', Project::MSG_ERR);
  433. $this->outputCustomFormatMessages($messages, 'ERROR');
  434. } else {
  435. $this->log($file . ': No syntax errors detected', Project::MSG_VERBOSE);
  436. }
  437. if ($warnings > 0) {
  438. $this->log($file . ': ' . $warnings . ' warning' . ($warnings > 1 ? 's' : '') . ' detected', Project::MSG_WARN);
  439. $this->outputCustomFormatMessages($messages, 'WARNING');
  440. }
  441. }
  442. $totalErrors = $report['totals']['errors'];
  443. $totalWarnings = $report['totals']['warnings'];
  444. $this->log(count($files) . ' files where checked', Project::MSG_INFO);
  445. if ($totalErrors > 0) {
  446. $this->log($totalErrors . ' error' . ($totalErrors > 1 ? 's' : '') . ' detected', Project::MSG_ERR);
  447. } else {
  448. $this->log('No syntax errors detected', Project::MSG_INFO);
  449. }
  450. if ($totalWarnings > 0) {
  451. $this->log($totalWarnings . ' warning' . ($totalWarnings > 1 ? 's' : '') . ' detected', Project::MSG_INFO);
  452. }
  453. }
  454. /**
  455. * Outputs the messages of a specific type for one file
  456. * @param array $messages
  457. * @param string $type
  458. */
  459. protected function outputCustomFormatMessages($messages, $type) {
  460. foreach ($messages as $line => $messagesPerLine) {
  461. foreach ($messagesPerLine as $column => $messagesPerColumn) {
  462. foreach ($messagesPerColumn as $message) {
  463. $msgType = $message['type'];
  464. if ($type == $msgType) {
  465. $logLevel = Project::MSG_INFO;
  466. if ($msgType == 'ERROR') {
  467. $logLevel = Project::MSG_ERR;
  468. } else if ($msgType == 'WARNING') {
  469. $logLevel = Project::MSG_WARN;
  470. }
  471. $text = $message['message'];
  472. $string = $msgType . ' in line ' . $line . ' column ' . $column . ': ' . $text;
  473. $this->log($string, $logLevel);
  474. }
  475. }
  476. }
  477. }
  478. }
  479. } //end phpCodeSnifferTask
  480. class PhpCodeSnifferTask_FormatterElement extends DataType {
  481. /**
  482. * Type of output to generate
  483. * @var string
  484. */
  485. protected $type = "";
  486. /**
  487. * Output to file?
  488. * @var bool
  489. */
  490. protected $useFile = true;
  491. /**
  492. * Output file.
  493. * @var string
  494. */
  495. protected $outfile = "";
  496. /**
  497. * Validate config.
  498. */
  499. public function parsingComplete () {
  500. if(empty($this->type)) {
  501. throw new BuildException("Format missing required 'type' attribute.");
  502. }
  503. if ($useFile && empty($this->outfile)) {
  504. throw new BuildException("Format requires 'outfile' attribute when 'useFile' is true.");
  505. }
  506. }
  507. public function setType ($type) {
  508. $this->type = $type;
  509. }
  510. public function getType () {
  511. return $this->type;
  512. }
  513. public function setUseFile ($useFile) {
  514. $this->useFile = $useFile;
  515. }
  516. public function getUseFile () {
  517. return $this->useFile;
  518. }
  519. public function setOutfile (PhingFile $outfile) {
  520. $this->outfile = $outfile;
  521. }
  522. public function getOutfile () {
  523. return $this->outfile;
  524. }
  525. } //end FormatterElement