PageRenderTime 44ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/php/PHP_CodeSniffer/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php

http://github.com/jonswar/perl-code-tidyall
PHP | 569 lines | 383 code | 75 blank | 111 comment | 61 complexity | 5510a0419b5802b3c34dab6f60014a01 MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-1.0, 0BSD, MIT
  1. <?php
  2. /**
  3. * Parses and verifies the doc comments for files.
  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\Standards\PEAR\Sniffs\Commenting;
  10. use PHP_CodeSniffer\Sniffs\Sniff;
  11. use PHP_CodeSniffer\Files\File;
  12. use PHP_CodeSniffer\Util\Common;
  13. class FileCommentSniff implements Sniff
  14. {
  15. /**
  16. * Tags in correct order and related info.
  17. *
  18. * @var array
  19. */
  20. protected $tags = [
  21. '@category' => [
  22. 'required' => true,
  23. 'allow_multiple' => false,
  24. ],
  25. '@package' => [
  26. 'required' => true,
  27. 'allow_multiple' => false,
  28. ],
  29. '@subpackage' => [
  30. 'required' => false,
  31. 'allow_multiple' => false,
  32. ],
  33. '@author' => [
  34. 'required' => true,
  35. 'allow_multiple' => true,
  36. ],
  37. '@copyright' => [
  38. 'required' => false,
  39. 'allow_multiple' => true,
  40. ],
  41. '@license' => [
  42. 'required' => true,
  43. 'allow_multiple' => false,
  44. ],
  45. '@version' => [
  46. 'required' => false,
  47. 'allow_multiple' => false,
  48. ],
  49. '@link' => [
  50. 'required' => true,
  51. 'allow_multiple' => true,
  52. ],
  53. '@see' => [
  54. 'required' => false,
  55. 'allow_multiple' => true,
  56. ],
  57. '@since' => [
  58. 'required' => false,
  59. 'allow_multiple' => false,
  60. ],
  61. '@deprecated' => [
  62. 'required' => false,
  63. 'allow_multiple' => false,
  64. ],
  65. ];
  66. /**
  67. * Returns an array of tokens this test wants to listen for.
  68. *
  69. * @return array
  70. */
  71. public function register()
  72. {
  73. return [T_OPEN_TAG];
  74. }//end register()
  75. /**
  76. * Processes this test, when one of its tokens is encountered.
  77. *
  78. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  79. * @param int $stackPtr The position of the current token
  80. * in the stack passed in $tokens.
  81. *
  82. * @return int
  83. */
  84. public function process(File $phpcsFile, $stackPtr)
  85. {
  86. $tokens = $phpcsFile->getTokens();
  87. // Find the next non whitespace token.
  88. $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
  89. // Allow declare() statements at the top of the file.
  90. if ($tokens[$commentStart]['code'] === T_DECLARE) {
  91. $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($commentStart + 1));
  92. $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($semicolon + 1), null, true);
  93. }
  94. // Ignore vim header.
  95. if ($tokens[$commentStart]['code'] === T_COMMENT) {
  96. if (strstr($tokens[$commentStart]['content'], 'vim:') !== false) {
  97. $commentStart = $phpcsFile->findNext(
  98. T_WHITESPACE,
  99. ($commentStart + 1),
  100. null,
  101. true
  102. );
  103. }
  104. }
  105. $errorToken = ($stackPtr + 1);
  106. if (isset($tokens[$errorToken]) === false) {
  107. $errorToken--;
  108. }
  109. if ($tokens[$commentStart]['code'] === T_CLOSE_TAG) {
  110. // We are only interested if this is the first open tag.
  111. return ($phpcsFile->numTokens + 1);
  112. } else if ($tokens[$commentStart]['code'] === T_COMMENT) {
  113. $error = 'You must use "/**" style comments for a file comment';
  114. $phpcsFile->addError($error, $errorToken, 'WrongStyle');
  115. $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes');
  116. return ($phpcsFile->numTokens + 1);
  117. } else if ($commentStart === false
  118. || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG
  119. ) {
  120. $phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing');
  121. $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'no');
  122. return ($phpcsFile->numTokens + 1);
  123. }
  124. $commentEnd = $tokens[$commentStart]['comment_closer'];
  125. $nextToken = $phpcsFile->findNext(
  126. T_WHITESPACE,
  127. ($commentEnd + 1),
  128. null,
  129. true
  130. );
  131. $ignore = [
  132. T_CLASS,
  133. T_INTERFACE,
  134. T_TRAIT,
  135. T_FUNCTION,
  136. T_CLOSURE,
  137. T_PUBLIC,
  138. T_PRIVATE,
  139. T_PROTECTED,
  140. T_FINAL,
  141. T_STATIC,
  142. T_ABSTRACT,
  143. T_CONST,
  144. T_PROPERTY,
  145. ];
  146. if (in_array($tokens[$nextToken]['code'], $ignore, true) === true) {
  147. $phpcsFile->addError('Missing file doc comment', $stackPtr, 'Missing');
  148. $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'no');
  149. return ($phpcsFile->numTokens + 1);
  150. }
  151. $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes');
  152. // Check the PHP Version, which should be in some text before the first tag.
  153. $found = false;
  154. for ($i = ($commentStart + 1); $i < $commentEnd; $i++) {
  155. if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) {
  156. break;
  157. } else if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING
  158. && strstr(strtolower($tokens[$i]['content']), 'php version') !== false
  159. ) {
  160. $found = true;
  161. break;
  162. }
  163. }
  164. if ($found === false) {
  165. $error = 'PHP version not specified';
  166. $phpcsFile->addWarning($error, $commentEnd, 'MissingVersion');
  167. }
  168. // Check each tag.
  169. $this->processTags($phpcsFile, $stackPtr, $commentStart);
  170. // Ignore the rest of the file.
  171. return ($phpcsFile->numTokens + 1);
  172. }//end process()
  173. /**
  174. * Processes each required or optional tag.
  175. *
  176. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  177. * @param int $stackPtr The position of the current token
  178. * in the stack passed in $tokens.
  179. * @param int $commentStart Position in the stack where the comment started.
  180. *
  181. * @return void
  182. */
  183. protected function processTags($phpcsFile, $stackPtr, $commentStart)
  184. {
  185. $tokens = $phpcsFile->getTokens();
  186. if (get_class($this) === 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FileCommentSniff') {
  187. $docBlock = 'file';
  188. } else {
  189. $docBlock = 'class';
  190. }
  191. $commentEnd = $tokens[$commentStart]['comment_closer'];
  192. $foundTags = [];
  193. $tagTokens = [];
  194. foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
  195. $name = $tokens[$tag]['content'];
  196. if (isset($this->tags[$name]) === false) {
  197. continue;
  198. }
  199. if ($this->tags[$name]['allow_multiple'] === false && isset($tagTokens[$name]) === true) {
  200. $error = 'Only one %s tag is allowed in a %s comment';
  201. $data = [
  202. $name,
  203. $docBlock,
  204. ];
  205. $phpcsFile->addError($error, $tag, 'Duplicate'.ucfirst(substr($name, 1)).'Tag', $data);
  206. }
  207. $foundTags[] = $name;
  208. $tagTokens[$name][] = $tag;
  209. $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
  210. if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
  211. $error = 'Content missing for %s tag in %s comment';
  212. $data = [
  213. $name,
  214. $docBlock,
  215. ];
  216. $phpcsFile->addError($error, $tag, 'Empty'.ucfirst(substr($name, 1)).'Tag', $data);
  217. continue;
  218. }
  219. }//end foreach
  220. // Check if the tags are in the correct position.
  221. $pos = 0;
  222. foreach ($this->tags as $tag => $tagData) {
  223. if (isset($tagTokens[$tag]) === false) {
  224. if ($tagData['required'] === true) {
  225. $error = 'Missing %s tag in %s comment';
  226. $data = [
  227. $tag,
  228. $docBlock,
  229. ];
  230. $phpcsFile->addError($error, $commentEnd, 'Missing'.ucfirst(substr($tag, 1)).'Tag', $data);
  231. }
  232. continue;
  233. } else {
  234. $method = 'process'.substr($tag, 1);
  235. if (method_exists($this, $method) === true) {
  236. // Process each tag if a method is defined.
  237. call_user_func([$this, $method], $phpcsFile, $tagTokens[$tag]);
  238. }
  239. }
  240. if (isset($foundTags[$pos]) === false) {
  241. break;
  242. }
  243. if ($foundTags[$pos] !== $tag) {
  244. $error = 'The tag in position %s should be the %s tag';
  245. $data = [
  246. ($pos + 1),
  247. $tag,
  248. ];
  249. $phpcsFile->addError($error, $tokens[$commentStart]['comment_tags'][$pos], ucfirst(substr($tag, 1)).'TagOrder', $data);
  250. }
  251. // Account for multiple tags.
  252. $pos++;
  253. while (isset($foundTags[$pos]) === true && $foundTags[$pos] === $tag) {
  254. $pos++;
  255. }
  256. }//end foreach
  257. }//end processTags()
  258. /**
  259. * Process the category tag.
  260. *
  261. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  262. * @param array $tags The tokens for these tags.
  263. *
  264. * @return void
  265. */
  266. protected function processCategory($phpcsFile, array $tags)
  267. {
  268. $tokens = $phpcsFile->getTokens();
  269. foreach ($tags as $tag) {
  270. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  271. // No content.
  272. continue;
  273. }
  274. $content = $tokens[($tag + 2)]['content'];
  275. if (Common::isUnderscoreName($content) !== true) {
  276. $newContent = str_replace(' ', '_', $content);
  277. $nameBits = explode('_', $newContent);
  278. $firstBit = array_shift($nameBits);
  279. $newName = ucfirst($firstBit).'_';
  280. foreach ($nameBits as $bit) {
  281. if ($bit !== '') {
  282. $newName .= ucfirst($bit).'_';
  283. }
  284. }
  285. $error = 'Category name "%s" is not valid; consider "%s" instead';
  286. $validName = trim($newName, '_');
  287. $data = [
  288. $content,
  289. $validName,
  290. ];
  291. $phpcsFile->addError($error, $tag, 'InvalidCategory', $data);
  292. }
  293. }//end foreach
  294. }//end processCategory()
  295. /**
  296. * Process the package tag.
  297. *
  298. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  299. * @param array $tags The tokens for these tags.
  300. *
  301. * @return void
  302. */
  303. protected function processPackage($phpcsFile, array $tags)
  304. {
  305. $tokens = $phpcsFile->getTokens();
  306. foreach ($tags as $tag) {
  307. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  308. // No content.
  309. continue;
  310. }
  311. $content = $tokens[($tag + 2)]['content'];
  312. if (Common::isUnderscoreName($content) === true) {
  313. continue;
  314. }
  315. $newContent = str_replace(' ', '_', $content);
  316. $newContent = trim($newContent, '_');
  317. $newContent = preg_replace('/[^A-Za-z_]/', '', $newContent);
  318. if ($newContent === '') {
  319. $error = 'Package name "%s" is not valid';
  320. $data = [$content];
  321. $phpcsFile->addError($error, $tag, 'InvalidPackageValue', $data);
  322. } else {
  323. $nameBits = explode('_', $newContent);
  324. $firstBit = array_shift($nameBits);
  325. $newName = strtoupper($firstBit[0]).substr($firstBit, 1).'_';
  326. foreach ($nameBits as $bit) {
  327. if ($bit !== '') {
  328. $newName .= strtoupper($bit[0]).substr($bit, 1).'_';
  329. }
  330. }
  331. $error = 'Package name "%s" is not valid; consider "%s" instead';
  332. $validName = trim($newName, '_');
  333. $data = [
  334. $content,
  335. $validName,
  336. ];
  337. $phpcsFile->addError($error, $tag, 'InvalidPackage', $data);
  338. }//end if
  339. }//end foreach
  340. }//end processPackage()
  341. /**
  342. * Process the subpackage tag.
  343. *
  344. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  345. * @param array $tags The tokens for these tags.
  346. *
  347. * @return void
  348. */
  349. protected function processSubpackage($phpcsFile, array $tags)
  350. {
  351. $tokens = $phpcsFile->getTokens();
  352. foreach ($tags as $tag) {
  353. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  354. // No content.
  355. continue;
  356. }
  357. $content = $tokens[($tag + 2)]['content'];
  358. if (Common::isUnderscoreName($content) === true) {
  359. continue;
  360. }
  361. $newContent = str_replace(' ', '_', $content);
  362. $nameBits = explode('_', $newContent);
  363. $firstBit = array_shift($nameBits);
  364. $newName = strtoupper($firstBit[0]).substr($firstBit, 1).'_';
  365. foreach ($nameBits as $bit) {
  366. if ($bit !== '') {
  367. $newName .= strtoupper($bit[0]).substr($bit, 1).'_';
  368. }
  369. }
  370. $error = 'Subpackage name "%s" is not valid; consider "%s" instead';
  371. $validName = trim($newName, '_');
  372. $data = [
  373. $content,
  374. $validName,
  375. ];
  376. $phpcsFile->addError($error, $tag, 'InvalidSubpackage', $data);
  377. }//end foreach
  378. }//end processSubpackage()
  379. /**
  380. * Process the author tag(s) that this header comment has.
  381. *
  382. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  383. * @param array $tags The tokens for these tags.
  384. *
  385. * @return void
  386. */
  387. protected function processAuthor($phpcsFile, array $tags)
  388. {
  389. $tokens = $phpcsFile->getTokens();
  390. foreach ($tags as $tag) {
  391. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  392. // No content.
  393. continue;
  394. }
  395. $content = $tokens[($tag + 2)]['content'];
  396. $local = '\da-zA-Z-_+';
  397. // Dot character cannot be the first or last character in the local-part.
  398. $localMiddle = $local.'.\w';
  399. if (preg_match('/^([^<]*)\s+<(['.$local.'](['.$localMiddle.']*['.$local.'])*@[\da-zA-Z][-.\w]*[\da-zA-Z]\.[a-zA-Z]{2,})>$/', $content) === 0) {
  400. $error = 'Content of the @author tag must be in the form "Display Name <username@example.com>"';
  401. $phpcsFile->addError($error, $tag, 'InvalidAuthors');
  402. }
  403. }
  404. }//end processAuthor()
  405. /**
  406. * Process the copyright tags.
  407. *
  408. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  409. * @param array $tags The tokens for these tags.
  410. *
  411. * @return void
  412. */
  413. protected function processCopyright($phpcsFile, array $tags)
  414. {
  415. $tokens = $phpcsFile->getTokens();
  416. foreach ($tags as $tag) {
  417. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  418. // No content.
  419. continue;
  420. }
  421. $content = $tokens[($tag + 2)]['content'];
  422. $matches = [];
  423. if (preg_match('/^([0-9]{4})((.{1})([0-9]{4}))? (.+)$/', $content, $matches) !== 0) {
  424. // Check earliest-latest year order.
  425. if ($matches[3] !== '' && $matches[3] !== null) {
  426. if ($matches[3] !== '-') {
  427. $error = 'A hyphen must be used between the earliest and latest year';
  428. $phpcsFile->addError($error, $tag, 'CopyrightHyphen');
  429. }
  430. if ($matches[4] !== '' && $matches[4] !== null && $matches[4] < $matches[1]) {
  431. $error = "Invalid year span \"$matches[1]$matches[3]$matches[4]\" found; consider \"$matches[4]-$matches[1]\" instead";
  432. $phpcsFile->addWarning($error, $tag, 'InvalidCopyright');
  433. }
  434. }
  435. } else {
  436. $error = '@copyright tag must contain a year and the name of the copyright holder';
  437. $phpcsFile->addError($error, $tag, 'IncompleteCopyright');
  438. }
  439. }//end foreach
  440. }//end processCopyright()
  441. /**
  442. * Process the license tag.
  443. *
  444. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  445. * @param array $tags The tokens for these tags.
  446. *
  447. * @return void
  448. */
  449. protected function processLicense($phpcsFile, array $tags)
  450. {
  451. $tokens = $phpcsFile->getTokens();
  452. foreach ($tags as $tag) {
  453. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  454. // No content.
  455. continue;
  456. }
  457. $content = $tokens[($tag + 2)]['content'];
  458. $matches = [];
  459. preg_match('/^([^\s]+)\s+(.*)/', $content, $matches);
  460. if (count($matches) !== 3) {
  461. $error = '@license tag must contain a URL and a license name';
  462. $phpcsFile->addError($error, $tag, 'IncompleteLicense');
  463. }
  464. }
  465. }//end processLicense()
  466. /**
  467. * Process the version tag.
  468. *
  469. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  470. * @param array $tags The tokens for these tags.
  471. *
  472. * @return void
  473. */
  474. protected function processVersion($phpcsFile, array $tags)
  475. {
  476. $tokens = $phpcsFile->getTokens();
  477. foreach ($tags as $tag) {
  478. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  479. // No content.
  480. continue;
  481. }
  482. $content = $tokens[($tag + 2)]['content'];
  483. if (strstr($content, 'CVS:') === false
  484. && strstr($content, 'SVN:') === false
  485. && strstr($content, 'GIT:') === false
  486. && strstr($content, 'HG:') === false
  487. ) {
  488. $error = 'Invalid version "%s" in file comment; consider "CVS: <cvs_id>" or "SVN: <svn_id>" or "GIT: <git_id>" or "HG: <hg_id>" instead';
  489. $data = [$content];
  490. $phpcsFile->addWarning($error, $tag, 'InvalidVersion', $data);
  491. }
  492. }
  493. }//end processVersion()
  494. }//end class