PageRenderTime 27ms CodeModel.GetById 8ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php

https://gitlab.com/yousafsyed/easternglamor
PHP | 818 lines | 538 code | 112 blank | 168 comment | 121 complexity | ec68d04ca4e28d55f4fbfbc51a55f9b9 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-2014 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-2014 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 function 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('|\p{Lu}|u', $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('|\p{Lu}|u', $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. $tokens = $this->currentFile->getTokens();
  369. for ($returnToken = $this->_functionToken; $returnToken < $endToken; $returnToken++) {
  370. if ($tokens[$returnToken]['code'] === T_CLOSURE) {
  371. $returnToken = $tokens[$returnToken]['scope_closer'];
  372. continue;
  373. }
  374. if ($tokens[$returnToken]['code'] === T_RETURN) {
  375. break;
  376. }
  377. }
  378. if ($returnToken !== $endToken) {
  379. // If the function is not returning anything, just
  380. // exiting, then there is no problem.
  381. $semicolon = $this->currentFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
  382. if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
  383. $error = 'Function return type is void, but function contains return statement';
  384. $this->currentFile->addError($error, $errorPos, 'InvalidReturnVoid');
  385. }
  386. }
  387. }
  388. } else if ($content !== 'mixed') {
  389. // If return type is not void, there needs to be a
  390. // returns statement somewhere in the function that
  391. // returns something.
  392. if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
  393. $endToken = $tokens[$this->_functionToken]['scope_closer'];
  394. $returnToken = $this->currentFile->findNext(T_RETURN, $this->_functionToken, $endToken);
  395. if ($returnToken === false) {
  396. $error = 'Function return type is not void, but function has no return statement';
  397. $this->currentFile->addError($error, $errorPos, 'InvalidNoReturn');
  398. } else {
  399. $semicolon = $this->currentFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
  400. if ($tokens[$semicolon]['code'] === T_SEMICOLON) {
  401. $error = 'Function return type is not void, but function is returning void here';
  402. $this->currentFile->addError($error, $returnToken, 'InvalidReturnNotVoid');
  403. }
  404. }
  405. }
  406. }//end if
  407. $spacing = substr_count($return->getWhitespaceBeforeValue(), ' ');
  408. if ($spacing !== 1) {
  409. $error = '@return tag indented incorrectly; expected 1 space but found %s';
  410. $data = array($spacing);
  411. $this->currentFile->addError($error, $errorPos, 'ReturnIndent', $data);
  412. }
  413. }//end if
  414. } else {
  415. $error = 'Missing @return tag in function comment';
  416. $this->currentFile->addError($error, $commentEnd, 'MissingReturn');
  417. }//end if
  418. } else {
  419. // No return tag for constructor and destructor.
  420. if ($return !== null) {
  421. $errorPos = ($commentStart + $return->getLine());
  422. $error = '@return tag is not required for constructor and destructor';
  423. $this->currentFile->addError($error, $errorPos, 'ReturnNotRequired');
  424. }
  425. }//end if
  426. }//end processReturn()
  427. /**
  428. * Process any throw tags that this function comment has.
  429. *
  430. * @param int $commentStart The position in the stack where the comment started.
  431. *
  432. * @return void
  433. */
  434. protected function processThrows($commentStart)
  435. {
  436. if (count($this->commentParser->getThrows()) === 0) {
  437. return;
  438. }
  439. $tagOrder = $this->commentParser->getTagOrders();
  440. $index = array_keys($this->commentParser->getTagOrders(), 'throws');
  441. foreach ($this->commentParser->getThrows() as $i => $throw) {
  442. $exception = $throw->getValue();
  443. $content = trim($throw->getComment());
  444. $errorPos = ($commentStart + $throw->getLine());
  445. if (empty($exception) === true) {
  446. $error = 'Exception type and comment missing for @throws tag in function comment';
  447. $this->currentFile->addError($error, $errorPos, 'InvalidThrows');
  448. } else if (empty($content) === true) {
  449. $error = 'Comment missing for @throws tag in function comment';
  450. $this->currentFile->addError($error, $errorPos, 'EmptyThrows');
  451. } else {
  452. // Starts with a capital letter and ends with a fullstop.
  453. $firstChar = $content{0};
  454. if (strtoupper($firstChar) !== $firstChar) {
  455. $error = '@throws tag comment must start with a capital letter';
  456. $this->currentFile->addError($error, $errorPos, 'ThrowsNotCapital');
  457. }
  458. $lastChar = $content[(strlen($content) - 1)];
  459. if ($lastChar !== '.') {
  460. $error = '@throws tag comment must end with a full stop';
  461. $this->currentFile->addError($error, $errorPos, 'ThrowsNoFullStop');
  462. }
  463. }
  464. $since = array_keys($tagOrder, 'since');
  465. if (count($since) === 1 && $this->_tagIndex !== 0) {
  466. $this->_tagIndex++;
  467. if ($index[$i] !== $this->_tagIndex) {
  468. $error = 'The @throws tag is in the wrong order; the tag follows @return';
  469. $this->currentFile->addError($error, $errorPos, 'ThrowsOrder');
  470. }
  471. }
  472. }//end foreach
  473. }//end processThrows()
  474. /**
  475. * Process the function parameter comments.
  476. *
  477. * @param int $commentStart The position in the stack where
  478. * the comment started.
  479. * @param int $commentEnd The position in the stack where
  480. * the comment ended.
  481. *
  482. * @return void
  483. */
  484. protected function processParams($commentStart, $commentEnd)
  485. {
  486. $realParams = $this->currentFile->getMethodParameters($this->_functionToken);
  487. $params = $this->commentParser->getParams();
  488. $foundParams = array();
  489. if (empty($params) === false) {
  490. if (substr_count($params[(count($params) - 1)]->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) {
  491. $error = 'Last parameter comment requires a blank newline after it';
  492. $errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
  493. $this->currentFile->addError($error, $errorPos, 'SpacingAfterParams');
  494. }
  495. // Parameters must appear immediately after the comment.
  496. if ($params[0]->getOrder() !== 2) {
  497. $error = 'Parameters must appear immediately after the comment';
  498. $errorPos = ($params[0]->getLine() + $commentStart);
  499. $this->currentFile->addError($error, $errorPos, 'SpacingBeforeParams');
  500. }
  501. $previousParam = null;
  502. $spaceBeforeVar = 10000;
  503. $spaceBeforeComment = 10000;
  504. $longestType = 0;
  505. $longestVar = 0;
  506. foreach ($params as $param) {
  507. $paramComment = trim($param->getComment());
  508. $errorPos = ($param->getLine() + $commentStart);
  509. // Make sure that there is only one space before the var type.
  510. if ($param->getWhitespaceBeforeType() !== ' ') {
  511. $error = 'Expected 1 space before variable type';
  512. $this->currentFile->addError($error, $errorPos, 'SpacingBeforeParamType');
  513. }
  514. $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
  515. if ($spaceCount < $spaceBeforeVar) {
  516. $spaceBeforeVar = $spaceCount;
  517. $longestType = $errorPos;
  518. }
  519. $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
  520. if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
  521. $spaceBeforeComment = $spaceCount;
  522. $longestVar = $errorPos;
  523. }
  524. // Make sure they are in the correct order, and have the correct name.
  525. $pos = $param->getPosition();
  526. $paramName = ($param->getVarName() !== '') ? $param->getVarName() : '[ UNKNOWN ]';
  527. if ($previousParam !== null) {
  528. $previousName = ($previousParam->getVarName() !== '') ? $previousParam->getVarName() : 'UNKNOWN';
  529. // Check to see if the parameters align properly.
  530. if ($param->alignsVariableWith($previousParam) === false) {
  531. $error = 'The variable names for parameters %s (%s) and %s (%s) do not align';
  532. $data = array(
  533. $previousName,
  534. ($pos - 1),
  535. $paramName,
  536. $pos,
  537. );
  538. $this->currentFile->addError($error, $errorPos, 'ParameterNamesNotAligned', $data);
  539. }
  540. if ($param->alignsCommentWith($previousParam) === false) {
  541. $error = 'The comments for parameters %s (%s) and %s (%s) do not align';
  542. $data = array(
  543. $previousName,
  544. ($pos - 1),
  545. $paramName,
  546. $pos,
  547. );
  548. $this->currentFile->addError($error, $errorPos, 'ParameterCommentsNotAligned', $data);
  549. }
  550. }
  551. // Variable must be one of the supported standard type.
  552. $typeNames = explode('|', $param->getType());
  553. foreach ($typeNames as $typeName) {
  554. $suggestedName = PHP_CodeSniffer::suggestType($typeName);
  555. if ($typeName !== $suggestedName) {
  556. $error = 'Expected "%s"; found "%s" for %s at position %s';
  557. $data = array(
  558. $suggestedName,
  559. $typeName,
  560. $paramName,
  561. $pos,
  562. );
  563. $this->currentFile->addError($error, $errorPos, 'IncorrectParamVarName', $data);
  564. } else if (count($typeNames) === 1) {
  565. // Check type hint for array and custom type.
  566. $suggestedTypeHint = '';
  567. if (strpos($suggestedName, 'array') !== false) {
  568. $suggestedTypeHint = 'array';
  569. } else if (strpos($suggestedName, 'callable') !== false) {
  570. $suggestedTypeHint = 'callable';
  571. } else if (in_array($typeName, PHP_CodeSniffer::$allowedTypes) === false) {
  572. $suggestedTypeHint = $suggestedName;
  573. }
  574. if ($suggestedTypeHint !== '' && isset($realParams[($pos - 1)]) === true) {
  575. $typeHint = $realParams[($pos - 1)]['type_hint'];
  576. if ($typeHint === '') {
  577. $error = 'Type hint "%s" missing for %s at position %s';
  578. $data = array(
  579. $suggestedTypeHint,
  580. $paramName,
  581. $pos,
  582. );
  583. $this->currentFile->addError($error, ($commentEnd + 2), 'TypeHintMissing', $data);
  584. } else if ($typeHint !== $suggestedTypeHint) {
  585. $error = 'Expected type hint "%s"; found "%s" for %s at position %s';
  586. $data = array(
  587. $suggestedTypeHint,
  588. $typeHint,
  589. $paramName,
  590. $pos,
  591. );
  592. $this->currentFile->addError($error, ($commentEnd + 2), 'IncorrectTypeHint', $data);
  593. }
  594. } else if ($suggestedTypeHint === '' && isset($realParams[($pos - 1)]) === true) {
  595. $typeHint = $realParams[($pos - 1)]['type_hint'];
  596. if ($typeHint !== '') {
  597. $error = 'Unknown type hint "%s" found for %s at position %s';
  598. $data = array(
  599. $typeHint,
  600. $paramName,
  601. $pos,
  602. );
  603. $this->currentFile->addError($error, ($commentEnd + 2), 'InvalidTypeHint', $data);
  604. }
  605. }
  606. }//end if
  607. }//end foreach
  608. // Make sure the names of the parameter comment matches the
  609. // actual parameter.
  610. if (isset($realParams[($pos - 1)]) === true) {
  611. $realName = $realParams[($pos - 1)]['name'];
  612. $foundParams[] = $realName;
  613. // Append ampersand to name if passing by reference.
  614. if ($realParams[($pos - 1)]['pass_by_reference'] === true) {
  615. $realName = '&'.$realName;
  616. }
  617. if ($realName !== $paramName) {
  618. $code = 'ParamNameNoMatch';
  619. $data = array(
  620. $paramName,
  621. $realName,
  622. $pos,
  623. );
  624. $error = 'Doc comment for var %s does not match ';
  625. if (strtolower($paramName) === strtolower($realName)) {
  626. $error .= 'case of ';
  627. $code = 'ParamNameNoCaseMatch';
  628. }
  629. $error .= 'actual variable name %s at position %s';
  630. $this->currentFile->addError($error, $errorPos, $code, $data);
  631. }
  632. } else if (substr($paramName, -4) !== ',...') {
  633. // We must have an extra parameter comment.
  634. $error = 'Superfluous doc comment at position '.$pos;
  635. $this->currentFile->addError($error, $errorPos, 'ExtraParamComment');
  636. }
  637. if ($param->getVarName() === '') {
  638. $error = 'Missing parameter name at position '.$pos;
  639. $this->currentFile->addError($error, $errorPos, 'MissingParamName');
  640. }
  641. if ($param->getType() === '') {
  642. $error = 'Missing type at position '.$pos;
  643. $this->currentFile->addError($error, $errorPos, 'MissingParamType');
  644. }
  645. if ($paramComment === '') {
  646. $error = 'Missing comment for param "%s" at position %s';
  647. $data = array(
  648. $paramName,
  649. $pos,
  650. );
  651. $this->currentFile->addError($error, $errorPos, 'MissingParamComment', $data);
  652. } else {
  653. // Param comments must start with a capital letter and
  654. // end with the full stop.
  655. $firstChar = $paramComment{0};
  656. if (preg_match('|\p{Lu}|u', $firstChar) === 0) {
  657. $error = 'Param comment must start with a capital letter';
  658. $this->currentFile->addError($error, $errorPos, 'ParamCommentNotCapital');
  659. }
  660. $lastChar = $paramComment[(strlen($paramComment) - 1)];
  661. if ($lastChar !== '.') {
  662. $error = 'Param comment must end with a full stop';
  663. $this->currentFile->addError($error, $errorPos, 'ParamCommentFullStop');
  664. }
  665. }
  666. $previousParam = $param;
  667. }//end foreach
  668. if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
  669. $error = 'Expected 1 space after the longest type';
  670. $this->currentFile->addError($error, $longestType, 'SpacingAfterLongType');
  671. }
  672. if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
  673. $error = 'Expected 1 space after the longest variable name';
  674. $this->currentFile->addError($error, $longestVar, 'SpacingAfterLongName');
  675. }
  676. }//end if
  677. $realNames = array();
  678. foreach ($realParams as $realParam) {
  679. $realNames[] = $realParam['name'];
  680. }
  681. // Report missing comments.
  682. $diff = array_diff($realNames, $foundParams);
  683. foreach ($diff as $neededParam) {
  684. if (count($params) !== 0) {
  685. $errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
  686. } else {
  687. $errorPos = $commentStart;
  688. }
  689. $error = 'Doc comment for "%s" missing';
  690. $data = array($neededParam);
  691. $this->currentFile->addError($error, $errorPos, 'MissingParamTag', $data);
  692. }
  693. }//end processParams()
  694. /**
  695. * Process a list of unknown tags.
  696. *
  697. * @param int $commentStart The position in the stack where the comment started.
  698. * @param int $commentEnd The position in the stack where the comment ended.
  699. *
  700. * @return void
  701. */
  702. protected function processUnknownTags($commentStart, $commentEnd)
  703. {
  704. }//end processUnknownTags
  705. }//end class
  706. ?>