PageRenderTime 49ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

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

http://github.com/squizlabs/PHP_CodeSniffer
PHP | 581 lines | 392 code | 78 blank | 111 comment | 66 complexity | b1e82f28aa6534c3b6acf0e3e449f807 MD5 | raw file
  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\Files\File;
  11. use PHP_CodeSniffer\Sniffs\Sniff;
  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. for ($nextToken = ($commentEnd + 1); $nextToken < $phpcsFile->numTokens; $nextToken++) {
  126. if ($tokens[$nextToken]['code'] === T_WHITESPACE) {
  127. continue;
  128. }
  129. if ($tokens[$nextToken]['code'] === T_ATTRIBUTE
  130. && isset($tokens[$nextToken]['attribute_closer']) === true
  131. ) {
  132. $nextToken = $tokens[$nextToken]['attribute_closer'];
  133. continue;
  134. }
  135. break;
  136. }
  137. if ($nextToken === $phpcsFile->numTokens) {
  138. $nextToken--;
  139. }
  140. $ignore = [
  141. T_CLASS,
  142. T_INTERFACE,
  143. T_TRAIT,
  144. T_FUNCTION,
  145. T_CLOSURE,
  146. T_PUBLIC,
  147. T_PRIVATE,
  148. T_PROTECTED,
  149. T_FINAL,
  150. T_STATIC,
  151. T_ABSTRACT,
  152. T_CONST,
  153. T_PROPERTY,
  154. ];
  155. if (in_array($tokens[$nextToken]['code'], $ignore, true) === true) {
  156. $phpcsFile->addError('Missing file doc comment', $stackPtr, 'Missing');
  157. $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'no');
  158. return ($phpcsFile->numTokens + 1);
  159. }
  160. $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes');
  161. // Check the PHP Version, which should be in some text before the first tag.
  162. $found = false;
  163. for ($i = ($commentStart + 1); $i < $commentEnd; $i++) {
  164. if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) {
  165. break;
  166. } else if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING
  167. && strstr(strtolower($tokens[$i]['content']), 'php version') !== false
  168. ) {
  169. $found = true;
  170. break;
  171. }
  172. }
  173. if ($found === false) {
  174. $error = 'PHP version not specified';
  175. $phpcsFile->addWarning($error, $commentEnd, 'MissingVersion');
  176. }
  177. // Check each tag.
  178. $this->processTags($phpcsFile, $stackPtr, $commentStart);
  179. // Ignore the rest of the file.
  180. return ($phpcsFile->numTokens + 1);
  181. }//end process()
  182. /**
  183. * Processes each required or optional tag.
  184. *
  185. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  186. * @param int $stackPtr The position of the current token
  187. * in the stack passed in $tokens.
  188. * @param int $commentStart Position in the stack where the comment started.
  189. *
  190. * @return void
  191. */
  192. protected function processTags($phpcsFile, $stackPtr, $commentStart)
  193. {
  194. $tokens = $phpcsFile->getTokens();
  195. if (get_class($this) === 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FileCommentSniff') {
  196. $docBlock = 'file';
  197. } else {
  198. $docBlock = 'class';
  199. }
  200. $commentEnd = $tokens[$commentStart]['comment_closer'];
  201. $foundTags = [];
  202. $tagTokens = [];
  203. foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
  204. $name = $tokens[$tag]['content'];
  205. if (isset($this->tags[$name]) === false) {
  206. continue;
  207. }
  208. if ($this->tags[$name]['allow_multiple'] === false && isset($tagTokens[$name]) === true) {
  209. $error = 'Only one %s tag is allowed in a %s comment';
  210. $data = [
  211. $name,
  212. $docBlock,
  213. ];
  214. $phpcsFile->addError($error, $tag, 'Duplicate'.ucfirst(substr($name, 1)).'Tag', $data);
  215. }
  216. $foundTags[] = $name;
  217. $tagTokens[$name][] = $tag;
  218. $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
  219. if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
  220. $error = 'Content missing for %s tag in %s comment';
  221. $data = [
  222. $name,
  223. $docBlock,
  224. ];
  225. $phpcsFile->addError($error, $tag, 'Empty'.ucfirst(substr($name, 1)).'Tag', $data);
  226. continue;
  227. }
  228. }//end foreach
  229. // Check if the tags are in the correct position.
  230. $pos = 0;
  231. foreach ($this->tags as $tag => $tagData) {
  232. if (isset($tagTokens[$tag]) === false) {
  233. if ($tagData['required'] === true) {
  234. $error = 'Missing %s tag in %s comment';
  235. $data = [
  236. $tag,
  237. $docBlock,
  238. ];
  239. $phpcsFile->addError($error, $commentEnd, 'Missing'.ucfirst(substr($tag, 1)).'Tag', $data);
  240. }
  241. continue;
  242. } else {
  243. $method = 'process'.substr($tag, 1);
  244. if (method_exists($this, $method) === true) {
  245. // Process each tag if a method is defined.
  246. call_user_func([$this, $method], $phpcsFile, $tagTokens[$tag]);
  247. }
  248. }
  249. if (isset($foundTags[$pos]) === false) {
  250. break;
  251. }
  252. if ($foundTags[$pos] !== $tag) {
  253. $error = 'The tag in position %s should be the %s tag';
  254. $data = [
  255. ($pos + 1),
  256. $tag,
  257. ];
  258. $phpcsFile->addError($error, $tokens[$commentStart]['comment_tags'][$pos], ucfirst(substr($tag, 1)).'TagOrder', $data);
  259. }
  260. // Account for multiple tags.
  261. $pos++;
  262. while (isset($foundTags[$pos]) === true && $foundTags[$pos] === $tag) {
  263. $pos++;
  264. }
  265. }//end foreach
  266. }//end processTags()
  267. /**
  268. * Process the category tag.
  269. *
  270. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  271. * @param array $tags The tokens for these tags.
  272. *
  273. * @return void
  274. */
  275. protected function processCategory($phpcsFile, array $tags)
  276. {
  277. $tokens = $phpcsFile->getTokens();
  278. foreach ($tags as $tag) {
  279. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  280. // No content.
  281. continue;
  282. }
  283. $content = $tokens[($tag + 2)]['content'];
  284. if (Common::isUnderscoreName($content) !== true) {
  285. $newContent = str_replace(' ', '_', $content);
  286. $nameBits = explode('_', $newContent);
  287. $firstBit = array_shift($nameBits);
  288. $newName = ucfirst($firstBit).'_';
  289. foreach ($nameBits as $bit) {
  290. if ($bit !== '') {
  291. $newName .= ucfirst($bit).'_';
  292. }
  293. }
  294. $error = 'Category name "%s" is not valid; consider "%s" instead';
  295. $validName = trim($newName, '_');
  296. $data = [
  297. $content,
  298. $validName,
  299. ];
  300. $phpcsFile->addError($error, $tag, 'InvalidCategory', $data);
  301. }
  302. }//end foreach
  303. }//end processCategory()
  304. /**
  305. * Process the package tag.
  306. *
  307. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  308. * @param array $tags The tokens for these tags.
  309. *
  310. * @return void
  311. */
  312. protected function processPackage($phpcsFile, array $tags)
  313. {
  314. $tokens = $phpcsFile->getTokens();
  315. foreach ($tags as $tag) {
  316. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  317. // No content.
  318. continue;
  319. }
  320. $content = $tokens[($tag + 2)]['content'];
  321. if (Common::isUnderscoreName($content) === true) {
  322. continue;
  323. }
  324. $newContent = str_replace(' ', '_', $content);
  325. $newContent = trim($newContent, '_');
  326. $newContent = preg_replace('/[^A-Za-z_]/', '', $newContent);
  327. if ($newContent === '') {
  328. $error = 'Package name "%s" is not valid';
  329. $data = [$content];
  330. $phpcsFile->addError($error, $tag, 'InvalidPackageValue', $data);
  331. } else {
  332. $nameBits = explode('_', $newContent);
  333. $firstBit = array_shift($nameBits);
  334. $newName = strtoupper($firstBit[0]).substr($firstBit, 1).'_';
  335. foreach ($nameBits as $bit) {
  336. if ($bit !== '') {
  337. $newName .= strtoupper($bit[0]).substr($bit, 1).'_';
  338. }
  339. }
  340. $error = 'Package name "%s" is not valid; consider "%s" instead';
  341. $validName = trim($newName, '_');
  342. $data = [
  343. $content,
  344. $validName,
  345. ];
  346. $phpcsFile->addError($error, $tag, 'InvalidPackage', $data);
  347. }//end if
  348. }//end foreach
  349. }//end processPackage()
  350. /**
  351. * Process the subpackage tag.
  352. *
  353. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  354. * @param array $tags The tokens for these tags.
  355. *
  356. * @return void
  357. */
  358. protected function processSubpackage($phpcsFile, array $tags)
  359. {
  360. $tokens = $phpcsFile->getTokens();
  361. foreach ($tags as $tag) {
  362. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  363. // No content.
  364. continue;
  365. }
  366. $content = $tokens[($tag + 2)]['content'];
  367. if (Common::isUnderscoreName($content) === true) {
  368. continue;
  369. }
  370. $newContent = str_replace(' ', '_', $content);
  371. $nameBits = explode('_', $newContent);
  372. $firstBit = array_shift($nameBits);
  373. $newName = strtoupper($firstBit[0]).substr($firstBit, 1).'_';
  374. foreach ($nameBits as $bit) {
  375. if ($bit !== '') {
  376. $newName .= strtoupper($bit[0]).substr($bit, 1).'_';
  377. }
  378. }
  379. $error = 'Subpackage name "%s" is not valid; consider "%s" instead';
  380. $validName = trim($newName, '_');
  381. $data = [
  382. $content,
  383. $validName,
  384. ];
  385. $phpcsFile->addError($error, $tag, 'InvalidSubpackage', $data);
  386. }//end foreach
  387. }//end processSubpackage()
  388. /**
  389. * Process the author tag(s) that this header comment has.
  390. *
  391. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  392. * @param array $tags The tokens for these tags.
  393. *
  394. * @return void
  395. */
  396. protected function processAuthor($phpcsFile, array $tags)
  397. {
  398. $tokens = $phpcsFile->getTokens();
  399. foreach ($tags as $tag) {
  400. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  401. // No content.
  402. continue;
  403. }
  404. $content = $tokens[($tag + 2)]['content'];
  405. $local = '\da-zA-Z-_+';
  406. // Dot character cannot be the first or last character in the local-part.
  407. $localMiddle = $local.'.\w';
  408. if (preg_match('/^([^<]*)\s+<(['.$local.'](['.$localMiddle.']*['.$local.'])*@[\da-zA-Z][-.\w]*[\da-zA-Z]\.[a-zA-Z]{2,})>$/', $content) === 0) {
  409. $error = 'Content of the @author tag must be in the form "Display Name <username@example.com>"';
  410. $phpcsFile->addError($error, $tag, 'InvalidAuthors');
  411. }
  412. }
  413. }//end processAuthor()
  414. /**
  415. * Process the copyright tags.
  416. *
  417. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  418. * @param array $tags The tokens for these tags.
  419. *
  420. * @return void
  421. */
  422. protected function processCopyright($phpcsFile, array $tags)
  423. {
  424. $tokens = $phpcsFile->getTokens();
  425. foreach ($tags as $tag) {
  426. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  427. // No content.
  428. continue;
  429. }
  430. $content = $tokens[($tag + 2)]['content'];
  431. $matches = [];
  432. if (preg_match('/^([0-9]{4})((.{1})([0-9]{4}))? (.+)$/', $content, $matches) !== 0) {
  433. // Check earliest-latest year order.
  434. if ($matches[3] !== '' && $matches[3] !== null) {
  435. if ($matches[3] !== '-') {
  436. $error = 'A hyphen must be used between the earliest and latest year';
  437. $phpcsFile->addError($error, $tag, 'CopyrightHyphen');
  438. }
  439. if ($matches[4] !== '' && $matches[4] !== null && $matches[4] < $matches[1]) {
  440. $error = "Invalid year span \"$matches[1]$matches[3]$matches[4]\" found; consider \"$matches[4]-$matches[1]\" instead";
  441. $phpcsFile->addWarning($error, $tag, 'InvalidCopyright');
  442. }
  443. }
  444. } else {
  445. $error = '@copyright tag must contain a year and the name of the copyright holder';
  446. $phpcsFile->addError($error, $tag, 'IncompleteCopyright');
  447. }
  448. }//end foreach
  449. }//end processCopyright()
  450. /**
  451. * Process the license tag.
  452. *
  453. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  454. * @param array $tags The tokens for these tags.
  455. *
  456. * @return void
  457. */
  458. protected function processLicense($phpcsFile, array $tags)
  459. {
  460. $tokens = $phpcsFile->getTokens();
  461. foreach ($tags as $tag) {
  462. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  463. // No content.
  464. continue;
  465. }
  466. $content = $tokens[($tag + 2)]['content'];
  467. $matches = [];
  468. preg_match('/^([^\s]+)\s+(.*)/', $content, $matches);
  469. if (count($matches) !== 3) {
  470. $error = '@license tag must contain a URL and a license name';
  471. $phpcsFile->addError($error, $tag, 'IncompleteLicense');
  472. }
  473. }
  474. }//end processLicense()
  475. /**
  476. * Process the version tag.
  477. *
  478. * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
  479. * @param array $tags The tokens for these tags.
  480. *
  481. * @return void
  482. */
  483. protected function processVersion($phpcsFile, array $tags)
  484. {
  485. $tokens = $phpcsFile->getTokens();
  486. foreach ($tags as $tag) {
  487. if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
  488. // No content.
  489. continue;
  490. }
  491. $content = $tokens[($tag + 2)]['content'];
  492. if (strstr($content, 'CVS:') === false
  493. && strstr($content, 'SVN:') === false
  494. && strstr($content, 'GIT:') === false
  495. && strstr($content, 'HG:') === false
  496. ) {
  497. $error = 'Invalid version "%s" in file comment; consider "CVS: <cvs_id>" or "SVN: <svn_id>" or "GIT: <git_id>" or "HG: <hg_id>" instead';
  498. $data = [$content];
  499. $phpcsFile->addWarning($error, $tag, 'InvalidVersion', $data);
  500. }
  501. }
  502. }//end processVersion()
  503. }//end class