PageRenderTime 53ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/CodeSniffer/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php

https://github.com/becoded/PHP_CodeSniffer
PHP | 810 lines | 533 code | 109 blank | 168 comment | 116 complexity | 89c0a9ffa8b4498ca8f8a9cf3f4af34e MD5 | raw file
  1. <?php
  2. /**
  3. * Parses and verifies the doc comments for functions.
  4. *
  5. * PHP version 5
  6. *
  7. * @category PHP
  8. * @package PHP_CodeSniffer
  9. * @author Greg Sherwood <gsherwood@squiz.net>
  10. * @author Marc McIntyre <mmcintyre@squiz.net>
  11. * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600)
  12. * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  13. * @link http://pear.php.net/package/PHP_CodeSniffer
  14. */
  15. if (class_exists('PHP_CodeSniffer_CommentParser_FunctionCommentParser', true) === false) {
  16. $error = 'Class PHP_CodeSniffer_CommentParser_FunctionCommentParser not found';
  17. throw new PHP_CodeSniffer_Exception($error);
  18. }
  19. /**
  20. * Parses and verifies the doc comments for functions.
  21. *
  22. * Verifies that :
  23. * <ul>
  24. * <li>A comment exists</li>
  25. * <li>There is a blank newline after the short description</li>
  26. * <li>There is a blank newline between the long and short description</li>
  27. * <li>There is a blank newline between the long description and tags</li>
  28. * <li>Parameter names represent those in the method</li>
  29. * <li>Parameter comments are in the correct order</li>
  30. * <li>Parameter comments are complete</li>
  31. * <li>A type hint is provided for array and custom class</li>
  32. * <li>Type hint matches the actual variable/class type</li>
  33. * <li>A blank line is present before the first and after the last parameter</li>
  34. * <li>A return type exists</li>
  35. * <li>Any throw tag must have a comment</li>
  36. * <li>The tag order and indentation are correct</li>
  37. * </ul>
  38. *
  39. * @category PHP
  40. * @package PHP_CodeSniffer
  41. * @author Greg Sherwood <gsherwood@squiz.net>
  42. * @author Marc McIntyre <mmcintyre@squiz.net>
  43. * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600)
  44. * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  45. * @version Release: @package_version@
  46. * @link http://pear.php.net/package/PHP_CodeSniffer
  47. */
  48. class Squiz_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_Sniff
  49. {
  50. /**
  51. * The name of the method that we are currently processing.
  52. *
  53. * @var string
  54. */
  55. private $_methodName = '';
  56. /**
  57. * The position in the stack where the fucntion token was found.
  58. *
  59. * @var int
  60. */
  61. private $_functionToken = null;
  62. /**
  63. * The position in the stack where the class token was found.
  64. *
  65. * @var int
  66. */
  67. private $_classToken = null;
  68. /**
  69. * The index of the current tag we are processing.
  70. *
  71. * @var int
  72. */
  73. private $_tagIndex = 0;
  74. /**
  75. * The function comment parser for the current method.
  76. *
  77. * @var PHP_CodeSniffer_Comment_Parser_FunctionCommentParser
  78. */
  79. protected $commentParser = null;
  80. /**
  81. * The current PHP_CodeSniffer_File object we are processing.
  82. *
  83. * @var PHP_CodeSniffer_File
  84. */
  85. protected $currentFile = null;
  86. /**
  87. * Returns an array of tokens this test wants to listen for.
  88. *
  89. * @return array
  90. */
  91. public function register()
  92. {
  93. return array(T_FUNCTION);
  94. }//end register()
  95. /**
  96. * Processes this test, when one of its tokens is encountered.
  97. *
  98. * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  99. * @param int $stackPtr The position of the current token
  100. * in the stack passed in $tokens.
  101. *
  102. * @return void
  103. */
  104. public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
  105. {
  106. $this->currentFile = $phpcsFile;
  107. $tokens = $phpcsFile->getTokens();
  108. $find = array(
  109. T_COMMENT,
  110. T_DOC_COMMENT,
  111. T_CLASS,
  112. T_FUNCTION,
  113. T_OPEN_TAG,
  114. );
  115. $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1));
  116. if ($commentEnd === false) {
  117. return;
  118. }
  119. // If the token that we found was a class or a function, then this
  120. // function has no doc comment.
  121. $code = $tokens[$commentEnd]['code'];
  122. if ($code === T_COMMENT) {
  123. // The function might actually be missing a comment, and this last comment
  124. // found is just commenting a bit of code on a line. So if it is not the
  125. // only thing on the line, assume we found nothing.
  126. $prevContent = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, $commentEnd);
  127. if ($tokens[$commentEnd]['line'] === $tokens[$commentEnd]['line']) {
  128. $error = 'Missing function doc comment';
  129. $phpcsFile->addError($error, $stackPtr, 'Missing');
  130. } else {
  131. $error = 'You must use "/**" style comments for a function comment';
  132. $phpcsFile->addError($error, $stackPtr, 'WrongStyle');
  133. }
  134. return;
  135. } else if ($code !== T_DOC_COMMENT) {
  136. $error = 'Missing function doc comment';
  137. $phpcsFile->addError($error, $stackPtr, 'Missing');
  138. return;
  139. } else if (trim($tokens[$commentEnd]['content']) !== '*/') {
  140. $error = 'You must use "*/" to end a function comment; found "%s"';
  141. $phpcsFile->addError($error, $commentEnd, 'WrongEnd', array(trim($tokens[$commentEnd]['content'])));
  142. return;
  143. }
  144. // If there is any code between the function keyword and the doc block
  145. // then the doc block is not for us.
  146. $ignore = PHP_CodeSniffer_Tokens::$scopeModifiers;
  147. $ignore[] = T_STATIC;
  148. $ignore[] = T_WHITESPACE;
  149. $ignore[] = T_ABSTRACT;
  150. $ignore[] = T_FINAL;
  151. $prevToken = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
  152. if ($prevToken !== $commentEnd) {
  153. $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
  154. return;
  155. }
  156. $this->_functionToken = $stackPtr;
  157. $this->_classToken = null;
  158. foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
  159. if ($condition === T_CLASS || $condition === T_INTERFACE) {
  160. $this->_classToken = $condPtr;
  161. break;
  162. }
  163. }
  164. // Find the first doc comment.
  165. $commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
  166. $commentString = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
  167. $this->_methodName = $phpcsFile->getDeclarationName($stackPtr);
  168. try {
  169. $this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($commentString, $phpcsFile);
  170. $this->commentParser->parse();
  171. } catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
  172. $line = ($e->getLineWithinComment() + $commentStart);
  173. $phpcsFile->addError($e->getMessage(), $line, 'FailedParse');
  174. return;
  175. }
  176. $comment = $this->commentParser->getComment();
  177. if (is_null($comment) === true) {
  178. $error = 'Function doc comment is empty';
  179. $phpcsFile->addError($error, $commentStart, 'Empty');
  180. return;
  181. }
  182. // The first line of the comment should just be the /** code.
  183. $eolPos = strpos($commentString, $phpcsFile->eolChar);
  184. $firstLine = substr($commentString, 0, $eolPos);
  185. if ($firstLine !== '/**') {
  186. $error = 'The open comment tag must be the only content on the line';
  187. $phpcsFile->addError($error, $commentStart, 'ContentAfterOpen');
  188. }
  189. $this->processParams($commentStart, $commentEnd);
  190. $this->processSees($commentStart);
  191. $this->processReturn($commentStart, $commentEnd);
  192. $this->processThrows($commentStart);
  193. // Check for a comment description.
  194. $short = $comment->getShortComment();
  195. if (trim($short) === '') {
  196. $error = 'Missing short description in function doc comment';
  197. $phpcsFile->addError($error, $commentStart, 'MissingShort');
  198. return;
  199. }
  200. // No extra newline before short description.
  201. $newlineCount = 0;
  202. $newlineSpan = strspn($short, $phpcsFile->eolChar);
  203. if ($short !== '' && $newlineSpan > 0) {
  204. $error = 'Extra newline(s) found before function comment short description';
  205. $phpcsFile->addError($error, ($commentStart + 1), 'SpacingBeforeShort');
  206. }
  207. $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
  208. // Exactly one blank line between short and long description.
  209. $long = $comment->getLongComment();
  210. if (empty($long) === false) {
  211. $between = $comment->getWhiteSpaceBetween();
  212. $newlineBetween = substr_count($between, $phpcsFile->eolChar);
  213. if ($newlineBetween !== 2) {
  214. $error = 'There must be exactly one blank line between descriptions in function comment';
  215. $phpcsFile->addError($error, ($commentStart + $newlineCount + 1), 'SpacingBetween');
  216. }
  217. $newlineCount += $newlineBetween;
  218. $testLong = trim($long);
  219. if (preg_match('|[A-Z]|', $testLong[0]) === 0) {
  220. $error = 'Function comment long description must start with a capital letter';
  221. $phpcsFile->addError($error, ($commentStart + $newlineCount), 'LongNotCapital');
  222. }
  223. }//end if
  224. // Exactly one blank line before tags.
  225. $params = $this->commentParser->getTagOrders();
  226. if (count($params) > 1) {
  227. $newlineSpan = $comment->getNewlineAfter();
  228. if ($newlineSpan !== 2) {
  229. $error = 'There must be exactly one blank line before the tags in function comment';
  230. if ($long !== '') {
  231. $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
  232. }
  233. $phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags');
  234. $short = rtrim($short, $phpcsFile->eolChar.' ');
  235. }
  236. }
  237. // Short description must be single line and end with a full stop.
  238. $testShort = trim($short);
  239. $lastChar = $testShort[(strlen($testShort) - 1)];
  240. if (substr_count($testShort, $phpcsFile->eolChar) !== 0) {
  241. $error = 'Function comment short description must be on a single line';
  242. $phpcsFile->addError($error, ($commentStart + 1), 'ShortSingleLine');
  243. }
  244. if (preg_match('|[A-Z]|', $testShort[0]) === 0) {
  245. $error = 'Function comment short description must start with a capital letter';
  246. $phpcsFile->addError($error, ($commentStart + 1), 'ShortNotCapital');
  247. }
  248. if ($lastChar !== '.') {
  249. $error = 'Function comment short description must end with a full stop';
  250. $phpcsFile->addError($error, ($commentStart + 1), 'ShortFullStop');
  251. }
  252. // Check for unknown/deprecated tags.
  253. $this->processUnknownTags($commentStart, $commentEnd);
  254. // The last content should be a newline and the content before
  255. // that should not be blank. If there is more blank space
  256. // then they have additional blank lines at the end of the comment.
  257. $words = $this->commentParser->getWords();
  258. $lastPos = (count($words) - 1);
  259. if (trim($words[($lastPos - 1)]) !== ''
  260. || strpos($words[($lastPos - 1)], $this->currentFile->eolChar) === false
  261. || trim($words[($lastPos - 2)]) === ''
  262. ) {
  263. $error = 'Additional blank lines found at end of function comment';
  264. $this->currentFile->addError($error, $commentEnd, 'SpacingAfter');
  265. }
  266. }//end process()
  267. /**
  268. * Process the see tags.
  269. *
  270. * @param int $commentStart The position in the stack where the comment started.
  271. *
  272. * @return void
  273. */
  274. protected function processSees($commentStart)
  275. {
  276. $sees = $this->commentParser->getSees();
  277. if (empty($sees) === false) {
  278. $tagOrder = $this->commentParser->getTagOrders();
  279. $index = array_keys($this->commentParser->getTagOrders(), 'see');
  280. foreach ($sees as $i => $see) {
  281. $errorPos = ($commentStart + $see->getLine());
  282. $since = array_keys($tagOrder, 'since');
  283. if (count($since) === 1 && $this->_tagIndex !== 0) {
  284. $this->_tagIndex++;
  285. if ($index[$i] !== $this->_tagIndex) {
  286. $error = 'The @see tag is in the wrong order; the tag precedes @return';
  287. $this->currentFile->addError($error, $errorPos, 'SeeOrder');
  288. }
  289. }
  290. $content = $see->getContent();
  291. if (empty($content) === true) {
  292. $error = 'Content missing for @see tag in function comment';
  293. $this->currentFile->addError($error, $errorPos, 'EmptySee');
  294. continue;
  295. }
  296. $spacing = substr_count($see->getWhitespaceBeforeContent(), ' ');
  297. if ($spacing !== 4) {
  298. $error = '@see tag indented incorrectly; expected 4 spaces but found %s';
  299. $data = array($spacing);
  300. $this->currentFile->addError($error, $errorPos, 'SeeIndent', $data);
  301. }
  302. }//end foreach
  303. }//end if
  304. }//end processSees()
  305. /**
  306. * Process the return comment of this function comment.
  307. *
  308. * @param int $commentStart The position in the stack where the comment started.
  309. * @param int $commentEnd The position in the stack where the comment ended.
  310. *
  311. * @return void
  312. */
  313. protected function processReturn($commentStart, $commentEnd)
  314. {
  315. // Skip constructor and destructor.
  316. $className = '';
  317. if ($this->_classToken !== null) {
  318. $className = $this->currentFile->getDeclarationName($this->_classToken);
  319. $className = strtolower(ltrim($className, '_'));
  320. }
  321. $methodName = strtolower(ltrim($this->_methodName, '_'));
  322. $isSpecialMethod = ($this->_methodName === '__construct' || $this->_methodName === '__destruct');
  323. $return = $this->commentParser->getReturn();
  324. if ($isSpecialMethod === false && $methodName !== $className) {
  325. if ($return !== null) {
  326. $tagOrder = $this->commentParser->getTagOrders();
  327. $index = array_keys($tagOrder, 'return');
  328. $errorPos = ($commentStart + $return->getLine());
  329. $content = trim($return->getRawContent());
  330. if (count($index) > 1) {
  331. $error = 'Only 1 @return tag is allowed in function comment';
  332. $this->currentFile->addError($error, $errorPos, 'DuplicateReturn');
  333. return;
  334. }
  335. $since = array_keys($tagOrder, 'since');
  336. if (count($since) === 1 && $this->_tagIndex !== 0) {
  337. $this->_tagIndex++;
  338. if ($index[0] !== $this->_tagIndex) {
  339. $error = 'The @return tag is in the wrong order; the tag follows @see (if used)';
  340. $this->currentFile->addError($error, $errorPos, 'ReturnOrder');
  341. }
  342. }
  343. if (empty($content) === true) {
  344. $error = 'Return type missing for @return tag in function comment';
  345. $this->currentFile->addError($error, $errorPos, 'MissingReturnType');
  346. } else {
  347. // Check return type (can be multiple, separated by '|').
  348. $typeNames = explode('|', $content);
  349. $suggestedNames = array();
  350. foreach ($typeNames as $i => $typeName) {
  351. $suggestedName = PHP_CodeSniffer::suggestType($typeName);
  352. if (in_array($suggestedName, $suggestedNames) === false) {
  353. $suggestedNames[] = $suggestedName;
  354. }
  355. }
  356. $suggestedType = implode('|', $suggestedNames);
  357. if ($content !== $suggestedType) {
  358. $error = 'Function return type "%s" is invalid';
  359. $data = array($content);
  360. $this->currentFile->addError($error, $errorPos, 'InvalidReturn', $data);
  361. }
  362. $tokens = $this->currentFile->getTokens();
  363. // If the return type is void, make sure there is
  364. // no return statement in the function.
  365. if ($content === 'void') {
  366. if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
  367. $endToken = $tokens[$this->_functionToken]['scope_closer'];
  368. $returnToken = $this->currentFile->findNext(T_RETURN, $this->_functionToken, $endToken);
  369. if ($returnToken !== false) {
  370. // If the function is not returning anything, just
  371. // exiting, then there is no problem.
  372. $semicolon = $this->currentFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
  373. if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
  374. $error = 'Function return type is void, but function contains return statement';
  375. $this->currentFile->addError($error, $errorPos, 'InvalidReturnVoid');
  376. }
  377. }
  378. }
  379. } else if ($content !== 'mixed') {
  380. // If return type is not void, there needs to be a
  381. // returns statement somewhere in the function that
  382. // returns something.
  383. if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
  384. $endToken = $tokens[$this->_functionToken]['scope_closer'];
  385. $returnToken = $this->currentFile->findNext(T_RETURN, $this->_functionToken, $endToken);
  386. if ($returnToken === false) {
  387. $error = 'Function return type is not void, but function has no return statement';
  388. $this->currentFile->addError($error, $errorPos, 'InvalidNoReturn');
  389. } else {
  390. $semicolon = $this->currentFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
  391. if ($tokens[$semicolon]['code'] === T_SEMICOLON) {
  392. $error = 'Function return type is not void, but function is returning void here';
  393. $this->currentFile->addError($error, $returnToken, 'InvalidReturnNotVoid');
  394. }
  395. }
  396. }
  397. }//end if
  398. $spacing = substr_count($return->getWhitespaceBeforeValue(), ' ');
  399. if ($spacing !== 1) {
  400. $error = '@return tag indented incorrectly; expected 1 space but found %s';
  401. $data = array($spacing);
  402. $this->currentFile->addError($error, $errorPos, 'ReturnIndent', $data);
  403. }
  404. }//end if
  405. } else {
  406. $error = 'Missing @return tag in function comment';
  407. $this->currentFile->addError($error, $commentEnd, 'MissingReturn');
  408. }//end if
  409. } else {
  410. // No return tag for constructor and destructor.
  411. if ($return !== null) {
  412. $errorPos = ($commentStart + $return->getLine());
  413. $error = '@return tag is not required for constructor and destructor';
  414. $this->currentFile->addError($error, $errorPos, 'ReturnNotRequired');
  415. }
  416. }//end if
  417. }//end processReturn()
  418. /**
  419. * Process any throw tags that this function comment has.
  420. *
  421. * @param int $commentStart The position in the stack where the comment started.
  422. *
  423. * @return void
  424. */
  425. protected function processThrows($commentStart)
  426. {
  427. if (count($this->commentParser->getThrows()) === 0) {
  428. return;
  429. }
  430. $tagOrder = $this->commentParser->getTagOrders();
  431. $index = array_keys($this->commentParser->getTagOrders(), 'throws');
  432. foreach ($this->commentParser->getThrows() as $i => $throw) {
  433. $exception = $throw->getValue();
  434. $content = trim($throw->getComment());
  435. $errorPos = ($commentStart + $throw->getLine());
  436. if (empty($exception) === true) {
  437. $error = 'Exception type and comment missing for @throws tag in function comment';
  438. $this->currentFile->addError($error, $errorPos, 'InvalidThrows');
  439. } else if (empty($content) === true) {
  440. $error = 'Comment missing for @throws tag in function comment';
  441. $this->currentFile->addError($error, $errorPos, 'EmptyThrows');
  442. } else {
  443. // Starts with a capital letter and ends with a fullstop.
  444. $firstChar = $content{0};
  445. if (strtoupper($firstChar) !== $firstChar) {
  446. $error = '@throws tag comment must start with a capital letter';
  447. $this->currentFile->addError($error, $errorPos, 'ThrowsNotCapital');
  448. }
  449. $lastChar = $content[(strlen($content) - 1)];
  450. if ($lastChar !== '.') {
  451. $error = '@throws tag comment must end with a full stop';
  452. $this->currentFile->addError($error, $errorPos, 'ThrowsNoFullStop');
  453. }
  454. }
  455. $since = array_keys($tagOrder, 'since');
  456. if (count($since) === 1 && $this->_tagIndex !== 0) {
  457. $this->_tagIndex++;
  458. if ($index[$i] !== $this->_tagIndex) {
  459. $error = 'The @throws tag is in the wrong order; the tag follows @return';
  460. $this->currentFile->addError($error, $errorPos, 'ThrowsOrder');
  461. }
  462. }
  463. }//end foreach
  464. }//end processThrows()
  465. /**
  466. * Process the function parameter comments.
  467. *
  468. * @param int $commentStart The position in the stack where
  469. * the comment started.
  470. * @param int $commentEnd The position in the stack where
  471. * the comment ended.
  472. *
  473. * @return void
  474. */
  475. protected function processParams($commentStart, $commentEnd)
  476. {
  477. $realParams = $this->currentFile->getMethodParameters($this->_functionToken);
  478. $params = $this->commentParser->getParams();
  479. $foundParams = array();
  480. if (empty($params) === false) {
  481. if (substr_count($params[(count($params) - 1)]->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) {
  482. $error = 'Last parameter comment requires a blank newline after it';
  483. $errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
  484. $this->currentFile->addError($error, $errorPos, 'SpacingAfterParams');
  485. }
  486. // Parameters must appear immediately after the comment.
  487. if ($params[0]->getOrder() !== 2) {
  488. $error = 'Parameters must appear immediately after the comment';
  489. $errorPos = ($params[0]->getLine() + $commentStart);
  490. $this->currentFile->addError($error, $errorPos, 'SpacingBeforeParams');
  491. }
  492. $previousParam = null;
  493. $spaceBeforeVar = 10000;
  494. $spaceBeforeComment = 10000;
  495. $longestType = 0;
  496. $longestVar = 0;
  497. foreach ($params as $param) {
  498. $paramComment = trim($param->getComment());
  499. $errorPos = ($param->getLine() + $commentStart);
  500. // Make sure that there is only one space before the var type.
  501. if ($param->getWhitespaceBeforeType() !== ' ') {
  502. $error = 'Expected 1 space before variable type';
  503. $this->currentFile->addError($error, $errorPos, 'SpacingBeforeParamType');
  504. }
  505. $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
  506. if ($spaceCount < $spaceBeforeVar) {
  507. $spaceBeforeVar = $spaceCount;
  508. $longestType = $errorPos;
  509. }
  510. $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
  511. if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
  512. $spaceBeforeComment = $spaceCount;
  513. $longestVar = $errorPos;
  514. }
  515. // Make sure they are in the correct order, and have the correct name.
  516. $pos = $param->getPosition();
  517. $paramName = ($param->getVarName() !== '') ? $param->getVarName() : '[ UNKNOWN ]';
  518. if ($previousParam !== null) {
  519. $previousName = ($previousParam->getVarName() !== '') ? $previousParam->getVarName() : 'UNKNOWN';
  520. // Check to see if the parameters align properly.
  521. if ($param->alignsVariableWith($previousParam) === false) {
  522. $error = 'The variable names for parameters %s (%s) and %s (%s) do not align';
  523. $data = array(
  524. $previousName,
  525. ($pos - 1),
  526. $paramName,
  527. $pos,
  528. );
  529. $this->currentFile->addError($error, $errorPos, 'ParameterNamesNotAligned', $data);
  530. }
  531. if ($param->alignsCommentWith($previousParam) === false) {
  532. $error = 'The comments for parameters %s (%s) and %s (%s) do not align';
  533. $data = array(
  534. $previousName,
  535. ($pos - 1),
  536. $paramName,
  537. $pos,
  538. );
  539. $this->currentFile->addError($error, $errorPos, 'ParameterCommentsNotAligned', $data);
  540. }
  541. }
  542. // Variable must be one of the supported standard type.
  543. $typeNames = explode('|', $param->getType());
  544. foreach ($typeNames as $typeName) {
  545. $suggestedName = PHP_CodeSniffer::suggestType($typeName);
  546. if ($typeName !== $suggestedName) {
  547. $error = 'Expected "%s"; found "%s" for %s at position %s';
  548. $data = array(
  549. $suggestedName,
  550. $typeName,
  551. $paramName,
  552. $pos,
  553. );
  554. $this->currentFile->addError($error, $errorPos, 'IncorrectParamVarName', $data);
  555. } else if (count($typeNames) === 1) {
  556. // Check type hint for array and custom type.
  557. $suggestedTypeHint = '';
  558. if (strpos($suggestedName, 'array') !== false) {
  559. $suggestedTypeHint = 'array';
  560. } else if (in_array($typeName, PHP_CodeSniffer::$allowedTypes) === false) {
  561. $suggestedTypeHint = $suggestedName;
  562. }
  563. if ($suggestedTypeHint !== '' && isset($realParams[($pos - 1)]) === true) {
  564. $typeHint = $realParams[($pos - 1)]['type_hint'];
  565. if ($typeHint === '') {
  566. $error = 'Type hint "%s" missing for %s at position %s';
  567. $data = array(
  568. $suggestedTypeHint,
  569. $paramName,
  570. $pos,
  571. );
  572. $this->currentFile->addError($error, ($commentEnd + 2), 'TypeHintMissing', $data);
  573. } else if ($typeHint !== $suggestedTypeHint) {
  574. $error = 'Expected type hint "%s"; found "%s" for %s at position %s';
  575. $data = array(
  576. $suggestedTypeHint,
  577. $typeHint,
  578. $paramName,
  579. $pos,
  580. );
  581. $this->currentFile->addError($error, ($commentEnd + 2), 'IncorrectTypeHint', $data);
  582. }
  583. } else if ($suggestedTypeHint === '' && isset($realParams[($pos - 1)]) === true) {
  584. $typeHint = $realParams[($pos - 1)]['type_hint'];
  585. if ($typeHint !== '') {
  586. $error = 'Unknown type hint "%s" found for %s at position %s';
  587. $data = array(
  588. $typeHint,
  589. $paramName,
  590. $pos,
  591. );
  592. $this->currentFile->addError($error, ($commentEnd + 2), 'InvalidTypeHint', $data);
  593. }
  594. }
  595. }//end if
  596. }//end foreach
  597. // Make sure the names of the parameter comment matches the
  598. // actual parameter.
  599. if (isset($realParams[($pos - 1)]) === true) {
  600. $realName = $realParams[($pos - 1)]['name'];
  601. $foundParams[] = $realName;
  602. // Append ampersand to name if passing by reference.
  603. if ($realParams[($pos - 1)]['pass_by_reference'] === true) {
  604. $realName = '&'.$realName;
  605. }
  606. if ($realName !== $paramName) {
  607. $code = 'ParamNameNoMatch';
  608. $data = array(
  609. $paramName,
  610. $realName,
  611. $pos,
  612. );
  613. $error = 'Doc comment for var %s does not match ';
  614. if (strtolower($paramName) === strtolower($realName)) {
  615. $error .= 'case of ';
  616. $code = 'ParamNameNoCaseMatch';
  617. }
  618. $error .= 'actual variable name %s at position %s';
  619. $this->currentFile->addError($error, $errorPos, $code, $data);
  620. }
  621. } else if (substr($paramName, -4) !== ',...') {
  622. // We must have an extra parameter comment.
  623. $error = 'Superfluous doc comment at position '.$pos;
  624. $this->currentFile->addError($error, $errorPos, 'ExtraParamComment');
  625. }
  626. if ($param->getVarName() === '') {
  627. $error = 'Missing parameter name at position '.$pos;
  628. $this->currentFile->addError($error, $errorPos, 'MissingParamName');
  629. }
  630. if ($param->getType() === '') {
  631. $error = 'Missing type at position '.$pos;
  632. $this->currentFile->addError($error, $errorPos, 'MissingParamType');
  633. }
  634. if ($paramComment === '') {
  635. $error = 'Missing comment for param "%s" at position %s';
  636. $data = array(
  637. $paramName,
  638. $pos,
  639. );
  640. $this->currentFile->addError($error, $errorPos, 'MissingParamComment', $data);
  641. } else {
  642. // Param comments must start with a capital letter and
  643. // end with the full stop.
  644. $firstChar = $paramComment{0};
  645. if (preg_match('|[A-Z]|', $firstChar) === 0) {
  646. $error = 'Param comment must start with a capital letter';
  647. $this->currentFile->addError($error, $errorPos, 'ParamCommentNotCapital');
  648. }
  649. $lastChar = $paramComment[(strlen($paramComment) - 1)];
  650. if ($lastChar !== '.') {
  651. $error = 'Param comment must end with a full stop';
  652. $this->currentFile->addError($error, $errorPos, 'ParamCommentFullStop');
  653. }
  654. }
  655. $previousParam = $param;
  656. }//end foreach
  657. if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
  658. $error = 'Expected 1 space after the longest type';
  659. $this->currentFile->addError($error, $longestType, 'SpacingAfterLongType');
  660. }
  661. if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
  662. $error = 'Expected 1 space after the longest variable name';
  663. $this->currentFile->addError($error, $longestVar, 'SpacingAfterLongName');
  664. }
  665. }//end if
  666. $realNames = array();
  667. foreach ($realParams as $realParam) {
  668. $realNames[] = $realParam['name'];
  669. }
  670. // Report missing comments.
  671. $diff = array_diff($realNames, $foundParams);
  672. foreach ($diff as $neededParam) {
  673. if (count($params) !== 0) {
  674. $errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
  675. } else {
  676. $errorPos = $commentStart;
  677. }
  678. $error = 'Doc comment for "%s" missing';
  679. $data = array($neededParam);
  680. $this->currentFile->addError($error, $errorPos, 'MissingParamTag', $data);
  681. }
  682. }//end processParams()
  683. /**
  684. * Process a list of unknown tags.
  685. *
  686. * @param int $commentStart The position in the stack where the comment started.
  687. * @param int $commentEnd The position in the stack where the comment ended.
  688. *
  689. * @return void
  690. */
  691. protected function processUnknownTags($commentStart, $commentEnd)
  692. {
  693. $unknownTags = $this->commentParser->getUnknown();
  694. foreach ($unknownTags as $errorTag) {
  695. $error = '@%s tag is not allowed in function comment';
  696. $data = array($errorTag['tag']);
  697. $this->currentFile->addWarning($error, ($commentStart + $errorTag['line']), 'TagNotAllowed', $data);
  698. }
  699. }//end processUnknownTags
  700. }//end class
  701. ?>