PageRenderTime 25ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/magento/magento2-base/dev/tests/static/framework/Magento/Sniffs/Annotations/RequireAnnotatedMethodsSniff.php

https://gitlab.com/yousafsyed/easternglamor
PHP | 694 lines | 475 code | 82 blank | 137 comment | 97 complexity | 03f6c581a20b02688a2f3187398372ff MD5 | raw file
  1. <?php
  2. namespace Magento\Sniffs\Annotations;
  3. use PHP_CodeSniffer_CommentParser_FunctionCommentParser;
  4. use PHP_CodeSniffer_CommentParser_ParserException;
  5. use PHP_CodeSniffer_File;
  6. use PHP_CodeSniffer_Sniff;
  7. use PHP_CodeSniffer_Tokens;
  8. include_once 'Helper.php';
  9. /**
  10. * Parses and verifies the doc comments for functions.
  11. *
  12. * PHP version 5
  13. *
  14. * @author Greg Sherwood <gsherwood@squiz.net>
  15. * @author Marc McIntyre <mmcintyre@squiz.net>
  16. * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600)
  17. * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
  18. * @link http://pear.php.net/package/PHP_CodeSniffer
  19. *
  20. * @SuppressWarnings(PHPMD)
  21. */
  22. class RequireAnnotatedMethodsSniff implements PHP_CodeSniffer_Sniff
  23. {
  24. /**
  25. * The name of the method that we are currently processing.
  26. *
  27. * @var string
  28. */
  29. private $_methodName = '';
  30. /**
  31. * The position in the stack where the function token was found.
  32. *
  33. * @var int
  34. */
  35. private $_functionToken = null;
  36. /**
  37. * The position in the stack where the class token was found.
  38. *
  39. * @var int
  40. */
  41. private $_classToken = null;
  42. /**
  43. * The index of the current tag we are processing.
  44. *
  45. * @var int
  46. */
  47. private $_tagIndex = 0;
  48. /**
  49. * The function comment parser for the current method.
  50. *
  51. * @var PHP_CodeSniffer_CommentParser_FunctionCommentParser
  52. */
  53. protected $commentParser = null;
  54. /**
  55. * The sniff helper for stuff shared between the annotations sniffs
  56. *
  57. * @var Helper
  58. */
  59. protected $helper = null;
  60. /**
  61. * Returns an array of tokens this test wants to listen for.
  62. *
  63. * @return array
  64. */
  65. public function register()
  66. {
  67. return [T_FUNCTION];
  68. }
  69. /**
  70. * Processes this test, when one of its tokens is encountered.
  71. *
  72. * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  73. * @param int $stackPtr The position of the current token
  74. * in the stack passed in $tokens.
  75. *
  76. * @return void
  77. */
  78. public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
  79. {
  80. $this->helper = new Helper($phpcsFile);
  81. // if we should skip this type we should do that
  82. if ($this->helper->shouldFilter()) {
  83. return;
  84. }
  85. $tokens = $phpcsFile->getTokens();
  86. $find = [T_COMMENT, T_DOC_COMMENT, T_CLASS, T_FUNCTION, T_OPEN_TAG];
  87. $commentEnd = $phpcsFile->findPrevious($find, $stackPtr - 1);
  88. if ($commentEnd === false) {
  89. return;
  90. }
  91. // If the token that we found was a class or a function, then this
  92. // function has no doc comment.
  93. $code = $tokens[$commentEnd]['code'];
  94. if ($code === T_COMMENT) {
  95. // The function might actually be missing a comment, and this last comment
  96. // found is just commenting a bit of code on a line. So if it is not the
  97. // only thing on the line, assume we found nothing.
  98. $prevContent = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, $commentEnd);
  99. if ($tokens[$commentEnd]['line'] === $tokens[$commentEnd]['line']) {
  100. $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']);
  101. } else {
  102. $this->helper->addMessage($stackPtr, Helper::WRONG_STYLE, ['function']);
  103. }
  104. return;
  105. } elseif ($code !== T_DOC_COMMENT) {
  106. $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']);
  107. return;
  108. } elseif (trim($tokens[$commentEnd]['content']) !== '*/') {
  109. $this->helper->addMessage($commentEnd, Helper::WRONG_END, [trim($tokens[$commentEnd]['content'])]);
  110. return;
  111. }
  112. // If there is any code between the function keyword and the doc block
  113. // then the doc block is not for us.
  114. $ignore = PHP_CodeSniffer_Tokens::$scopeModifiers;
  115. $ignore[] = T_STATIC;
  116. $ignore[] = T_WHITESPACE;
  117. $ignore[] = T_ABSTRACT;
  118. $ignore[] = T_FINAL;
  119. $prevToken = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true);
  120. if ($prevToken !== $commentEnd) {
  121. $this->helper->addMessage($stackPtr, Helper::MISSING, ['function']);
  122. return;
  123. }
  124. $this->_functionToken = $stackPtr;
  125. $this->_classToken = null;
  126. foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
  127. if ($condition === T_CLASS || $condition === T_INTERFACE) {
  128. $this->_classToken = $condPtr;
  129. break;
  130. }
  131. }
  132. // Find the first doc comment.
  133. $commentStart = $phpcsFile->findPrevious(T_DOC_COMMENT, $commentEnd - 1, null, true) + 1;
  134. $commentString = $phpcsFile->getTokensAsString($commentStart, $commentEnd - $commentStart + 1);
  135. $this->_methodName = $phpcsFile->getDeclarationName($stackPtr);
  136. try {
  137. $this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($commentString, $phpcsFile);
  138. $this->commentParser->parse();
  139. } catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
  140. $line = $e->getLineWithinComment() + $commentStart;
  141. $this->helper->addMessage($line, Helper::FAILED_PARSE, [$e->getMessage()]);
  142. return;
  143. }
  144. $comment = $this->commentParser->getComment();
  145. if (($comment === null) === true) {
  146. $this->helper->addMessage($commentStart, Helper::EMPTY_DOC, ['Function']);
  147. return;
  148. }
  149. // The first line of the comment should just be the /** code.
  150. $eolPos = strpos($commentString, $phpcsFile->eolChar);
  151. $firstLine = substr($commentString, 0, $eolPos);
  152. if ($firstLine !== '/**') {
  153. $this->helper->addMessage($commentStart, Helper::CONTENT_AFTER_OPEN);
  154. }
  155. // If the comment has an inherit doc note just move on
  156. if (preg_match('/\{\@inheritdoc\}/', $commentString)) {
  157. return;
  158. } elseif (preg_match('/\{?\@?inherit[dD]oc\}?/', $commentString)) {
  159. $this->helper->addMessage($commentStart, Helper::INCORRECT_INHERIT_DOC);
  160. return;
  161. }
  162. $this->processParams($commentStart, $commentEnd);
  163. $this->processSees($commentStart);
  164. $this->processReturn($commentStart, $commentEnd);
  165. $this->processThrows($commentStart);
  166. // Check for a comment description.
  167. $short = $comment->getShortComment();
  168. if (trim($short) === '') {
  169. $this->helper->addMessage($commentStart, Helper::MISSING_SHORT, ['function']);
  170. return;
  171. }
  172. // No extra newline before short description.
  173. $newlineCount = 0;
  174. $newlineSpan = strspn($short, $phpcsFile->eolChar);
  175. if ($short !== '' && $newlineSpan > 0) {
  176. $this->helper->addMessage($commentStart + 1, Helper::SPACING_BEFORE_SHORT, ['function']);
  177. }
  178. $newlineCount = substr_count($short, $phpcsFile->eolChar) + 1;
  179. // Exactly one blank line between short and long description.
  180. $long = $comment->getLongComment();
  181. if (empty($long) === false) {
  182. $between = $comment->getWhiteSpaceBetween();
  183. $newlineBetween = substr_count($between, $phpcsFile->eolChar);
  184. if ($newlineBetween !== 2) {
  185. $this->helper->addMessage(
  186. $commentStart + $newlineCount + 1,
  187. Helper::SPACING_BETWEEN,
  188. ['function']
  189. );
  190. }
  191. $newlineCount += $newlineBetween;
  192. $testLong = trim($long);
  193. if (preg_match('|\p{Lu}|u', $testLong[0]) === 0) {
  194. $this->helper->addMessage($commentStart + $newlineCount, Helper::LONG_NOT_CAPITAL, ['Function']);
  195. }
  196. }
  197. // Exactly one blank line before tags.
  198. $params = $this->commentParser->getTagOrders();
  199. if (count($params) > 1) {
  200. $newlineSpan = $comment->getNewlineAfter();
  201. if ($newlineSpan !== 2) {
  202. if ($long !== '') {
  203. $newlineCount += substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1;
  204. }
  205. $this->helper->addMessage(
  206. $commentStart + $newlineCount,
  207. Helper::SPACING_BEFORE_TAGS,
  208. ['function']
  209. );
  210. $short = rtrim($short, $phpcsFile->eolChar . ' ');
  211. }
  212. }
  213. // Short description must be single line and end with a full stop.
  214. $testShort = trim($short);
  215. $lastChar = $testShort[strlen($testShort) - 1];
  216. if (substr_count($testShort, $phpcsFile->eolChar) !== 0) {
  217. $this->helper->addMessage($commentStart + 1, Helper::SHORT_SINGLE_LINE, ['Function']);
  218. }
  219. if (preg_match('|\p{Lu}|u', $testShort[0]) === 0) {
  220. $this->helper->addMessage($commentStart + 1, Helper::SHORT_NOT_CAPITAL, ['Function']);
  221. }
  222. if ($lastChar !== '.') {
  223. $this->helper->addMessage($commentStart + 1, Helper::SHORT_FULL_STOP, ['Function']);
  224. }
  225. // Check for unknown/deprecated tags.
  226. // For example call: $this->processUnknownTags($commentStart, $commentEnd);
  227. // The last content should be a newline and the content before
  228. // that should not be blank. If there is more blank space
  229. // then they have additional blank lines at the end of the comment.
  230. $words = $this->commentParser->getWords();
  231. $lastPos = count($words) - 1;
  232. if (trim(
  233. $words[$lastPos - 1]
  234. ) !== '' || strpos(
  235. $words[$lastPos - 1],
  236. $this->helper->getCurrentFile()->eolChar
  237. ) === false || trim(
  238. $words[$lastPos - 2]
  239. ) === ''
  240. ) {
  241. $this->helper->addMessage($commentEnd, Helper::SPACING_AFTER, ['function']);
  242. }
  243. }
  244. /**
  245. * Process the see tags.
  246. *
  247. * @param int $commentStart The position in the stack where the comment started.
  248. *
  249. * @return void
  250. */
  251. protected function processSees($commentStart)
  252. {
  253. $sees = $this->commentParser->getSees();
  254. if (empty($sees) === false) {
  255. $tagOrder = $this->commentParser->getTagOrders();
  256. $index = array_keys($this->commentParser->getTagOrders(), 'see');
  257. foreach ($sees as $i => $see) {
  258. $errorPos = $commentStart + $see->getLine();
  259. $since = array_keys($tagOrder, 'since');
  260. if (count($since) === 1 && $this->_tagIndex !== 0) {
  261. $this->_tagIndex++;
  262. if ($index[$i] !== $this->_tagIndex) {
  263. $this->helper->addMessage($errorPos, Helper::SEE_ORDER);
  264. }
  265. }
  266. $content = $see->getContent();
  267. if (empty($content) === true) {
  268. $this->helper->addMessage($errorPos, Helper::EMPTY_SEE, ['function']);
  269. continue;
  270. }
  271. }
  272. }
  273. }
  274. /**
  275. * Process the return comment of this function comment.
  276. *
  277. * @param int $commentStart The position in the stack where the comment started.
  278. * @param int $commentEnd The position in the stack where the comment ended.
  279. *
  280. * @return void
  281. */
  282. protected function processReturn($commentStart, $commentEnd)
  283. {
  284. // Skip constructor and destructor.
  285. $className = '';
  286. if ($this->_classToken !== null) {
  287. $className = $this->helper->getCurrentFile()->getDeclarationName($this->_classToken);
  288. $className = strtolower(ltrim($className, '_'));
  289. }
  290. $methodName = strtolower(ltrim($this->_methodName, '_'));
  291. $return = $this->commentParser->getReturn();
  292. if ($this->_methodName !== '__construct' && $this->_methodName !== '__destruct') {
  293. if ($return !== null) {
  294. $tagOrder = $this->commentParser->getTagOrders();
  295. $index = array_keys($tagOrder, 'return');
  296. $errorPos = $commentStart + $return->getLine();
  297. $content = trim($return->getRawContent());
  298. if (count($index) > 1) {
  299. $this->helper->addMessage($errorPos, Helper::DUPLICATE_RETURN);
  300. return;
  301. }
  302. $since = array_keys($tagOrder, 'since');
  303. if (count($since) === 1 && $this->_tagIndex !== 0) {
  304. $this->_tagIndex++;
  305. if ($index[0] !== $this->_tagIndex) {
  306. $this->helper->addMessage($errorPos, Helper::RETURN_ORDER);
  307. }
  308. }
  309. if (empty($content) === true) {
  310. $this->helper->addMessage($errorPos, Helper::MISSING_RETURN_TYPE);
  311. } else {
  312. // Strip off any comments attached to our content
  313. $parts = explode(' ', $content);
  314. $content = $parts[0];
  315. // Check return type (can be multiple, separated by '|').
  316. $typeNames = explode('|', $content);
  317. $suggestedNames = [];
  318. foreach ($typeNames as $i => $typeName) {
  319. $suggestedName = $this->helper->suggestType($typeName);
  320. if (in_array($suggestedName, $suggestedNames) === false) {
  321. $suggestedNames[] = $suggestedName;
  322. }
  323. }
  324. $suggestedType = implode('|', $suggestedNames);
  325. if ($content !== $suggestedType) {
  326. $data = [$content];
  327. $this->helper->addMessage($errorPos, Helper::INVALID_RETURN, $data);
  328. } elseif ($this->helper->isAmbiguous($typeName, $matches)) {
  329. // Warn about ambiguous types ie array or mixed
  330. $data = [$matches[1], '@return'];
  331. $this->helper->addMessage($errorPos, Helper::AMBIGUOUS_TYPE, $data);
  332. }
  333. $tokens = $this->helper->getCurrentFile()->getTokens();
  334. // If the return type is void, make sure there is
  335. // no return statement in the function.
  336. if ($content === 'void') {
  337. if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
  338. $endToken = $tokens[$this->_functionToken]['scope_closer'];
  339. $tokens = $this->helper->getCurrentFile()->getTokens();
  340. for ($returnToken = $this->_functionToken; $returnToken < $endToken; $returnToken++) {
  341. if ($tokens[$returnToken]['code'] === T_CLOSURE) {
  342. $returnToken = $tokens[$returnToken]['scope_closer'];
  343. continue;
  344. }
  345. if ($tokens[$returnToken]['code'] === T_RETURN) {
  346. break;
  347. }
  348. }
  349. if ($returnToken !== $endToken) {
  350. // If the function is not returning anything, just
  351. // exiting, then there is no problem.
  352. $semicolon = $this->helper->getCurrentFile()->findNext(
  353. T_WHITESPACE,
  354. $returnToken + 1,
  355. null,
  356. true
  357. );
  358. if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
  359. $this->helper->addMessage($errorPos, Helper::INVALID_RETURN_VOID);
  360. }
  361. }
  362. }
  363. } elseif ($content !== 'mixed') {
  364. // If return type is not void, there needs to be a
  365. // returns statement somewhere in the function that
  366. // returns something.
  367. if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
  368. $endToken = $tokens[$this->_functionToken]['scope_closer'];
  369. $returnToken = $this->helper->getCurrentFile()->findNext(
  370. T_RETURN,
  371. $this->_functionToken,
  372. $endToken
  373. );
  374. if ($returnToken === false) {
  375. $this->helper->addMessage($errorPos, Helper::INVALID_NO_RETURN);
  376. } else {
  377. $semicolon = $this->helper->getCurrentFile()->findNext(
  378. T_WHITESPACE,
  379. $returnToken + 1,
  380. null,
  381. true
  382. );
  383. if ($tokens[$semicolon]['code'] === T_SEMICOLON) {
  384. $this->helper->addMessage($returnToken, Helper::INVALID_RETURN_NOT_VOID);
  385. }
  386. }
  387. }
  388. }
  389. $spacing = substr_count($return->getWhitespaceBeforeValue(), ' ');
  390. if ($spacing !== 1) {
  391. $data = [$spacing];
  392. $this->helper->addMessage($errorPos, Helper::RETURN_INDENT, $data);
  393. }
  394. }
  395. } else {
  396. $this->helper->addMessage($commentEnd, Helper::MISSING_RETURN);
  397. }
  398. } elseif ($return !== null) {
  399. // No return tag for constructor and destructor.
  400. $errorPos = $commentStart + $return->getLine();
  401. $this->helper->addMessage($errorPos, Helper::RETURN_NOT_REQUIRED);
  402. }
  403. }
  404. /**
  405. * Process any throw tags that this function comment has.
  406. *
  407. * @param int $commentStart The position in the stack where the comment started.
  408. *
  409. * @return void
  410. */
  411. protected function processThrows($commentStart)
  412. {
  413. if (count($this->commentParser->getThrows()) === 0) {
  414. return;
  415. }
  416. $tagOrder = $this->commentParser->getTagOrders();
  417. $index = array_keys($this->commentParser->getTagOrders(), 'throws');
  418. foreach ($this->commentParser->getThrows() as $i => $throw) {
  419. $exception = $throw->getValue();
  420. $content = trim($throw->getComment());
  421. $errorPos = $commentStart + $throw->getLine();
  422. if (empty($exception) === true) {
  423. $this->helper->addMessage($errorPos, Helper::INVALID_THROWS);
  424. } elseif (empty($content) === true) {
  425. $this->helper->addMessage($errorPos, Helper::EMPTY_THROWS);
  426. } else {
  427. // Assumes that $content is not empty.
  428. // Starts with a capital letter and ends with a fullstop.
  429. $firstChar = $content[0];
  430. if (strtoupper($firstChar) !== $firstChar) {
  431. $this->helper->addMessage($errorPos, Helper::THROWS_NOT_CAPITAL);
  432. }
  433. $lastChar = $content[strlen($content) - 1];
  434. if ($lastChar !== '.') {
  435. $this->helper->addMessage($errorPos, Helper::THROWS_NO_FULL_STOP);
  436. }
  437. }
  438. $since = array_keys($tagOrder, 'since');
  439. if (count($since) === 1 && $this->_tagIndex !== 0) {
  440. $this->_tagIndex++;
  441. if ($index[$i] !== $this->_tagIndex) {
  442. $this->helper->addMessage($errorPos, Helper::THROWS_ORDER);
  443. }
  444. }
  445. }
  446. }
  447. /**
  448. * Process the function parameter comments.
  449. *
  450. * @param int $commentStart The position in the stack where
  451. * the comment started.
  452. * @param int $commentEnd The position in the stack where
  453. * the comment ended.
  454. *
  455. * @return void
  456. */
  457. protected function processParams($commentStart, $commentEnd)
  458. {
  459. $realParams = $this->helper->getCurrentFile()->getMethodParameters($this->_functionToken);
  460. $params = $this->commentParser->getParams();
  461. $foundParams = [];
  462. if (empty($params) === false) {
  463. $subStrCount = substr_count(
  464. $params[count($params) - 1]->getWhitespaceAfter(),
  465. $this->helper->getCurrentFile()->eolChar
  466. );
  467. if ($subStrCount !== 2) {
  468. $errorPos = $params[count($params) - 1]->getLine() + $commentStart;
  469. $this->helper->addMessage($errorPos, Helper::SPACING_AFTER_PARAMS);
  470. }
  471. // Parameters must appear immediately after the comment.
  472. if ($params[0]->getOrder() !== 2) {
  473. $errorPos = $params[0]->getLine() + $commentStart;
  474. $this->helper->addMessage($errorPos, Helper::SPACING_BEFORE_PARAMS);
  475. }
  476. $previousParam = null;
  477. $spaceBeforeVar = 10000;
  478. $spaceBeforeComment = 10000;
  479. $longestType = 0;
  480. $longestVar = 0;
  481. foreach ($params as $param) {
  482. $paramComment = trim($param->getComment());
  483. $errorPos = $param->getLine() + $commentStart;
  484. // Make sure that there is only one space before the var type.
  485. if ($param->getWhitespaceBeforeType() !== ' ') {
  486. $this->helper->addMessage($errorPos, Helper::SPACING_BEFORE_PARAM_TYPE);
  487. }
  488. $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
  489. if ($spaceCount < $spaceBeforeVar) {
  490. $spaceBeforeVar = $spaceCount;
  491. $longestType = $errorPos;
  492. }
  493. $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
  494. if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
  495. $spaceBeforeComment = $spaceCount;
  496. $longestVar = $errorPos;
  497. }
  498. // Make sure they are in the correct order, and have the correct name.
  499. $pos = $param->getPosition();
  500. $paramName = $param->getVarName() !== '' ? $param->getVarName() : '[ UNKNOWN ]';
  501. if ($previousParam !== null) {
  502. $previousName = $previousParam->getVarName() !== '' ? $previousParam->getVarName() : 'UNKNOWN';
  503. }
  504. // Variable must be one of the supported standard type.
  505. $typeNames = explode('|', $param->getType());
  506. foreach ($typeNames as $typeName) {
  507. $suggestedName = $this->helper->suggestType($typeName);
  508. if ($typeName !== $suggestedName) {
  509. $data = [$suggestedName, $typeName, $paramName, $pos];
  510. $this->helper->addMessage($errorPos, Helper::INCORRECT_PARAM_VAR_NAME, $data);
  511. } elseif ($this->helper->isAmbiguous($typeName, $matches)) {
  512. // Warn about ambiguous types ie array or mixed
  513. $data = [$matches[1], $paramName, ' at position ' . $pos . ' is NOT recommended'];
  514. $this->helper->addMessage($commentEnd + 2, Helper::AMBIGUOUS_TYPE, $data);
  515. } elseif (count($typeNames) === 1) {
  516. // Check type hint for array and custom type.
  517. $suggestedTypeHint = '';
  518. if (strpos($suggestedName, 'array') !== false) {
  519. $suggestedTypeHint = 'array';
  520. } elseif (strpos($suggestedName, 'callable') !== false) {
  521. $suggestedTypeHint = 'callable';
  522. } elseif (in_array($typeName, $this->helper->getAllowedTypes()) === false) {
  523. $suggestedTypeHint = $suggestedName;
  524. } else {
  525. $suggestedTypeHint = $this->helper->suggestType($typeName);
  526. }
  527. if ($suggestedTypeHint !== '' && isset($realParams[$pos - 1]) === true) {
  528. $typeHint = $realParams[$pos - 1]['type_hint'];
  529. if ($typeHint === '') {
  530. $data = [$suggestedTypeHint, $paramName, $pos];
  531. $this->helper->addMessage($commentEnd + 2, Helper::TYPE_HINT_MISSING, $data);
  532. } elseif ($typeHint !== $suggestedTypeHint) {
  533. $data = [$suggestedTypeHint, $typeHint, $paramName, $pos];
  534. $this->helper->addMessage($commentEnd + 2, Helper::INCORRECT_TYPE_HINT, $data);
  535. }
  536. } elseif ($suggestedTypeHint === '' && isset($realParams[$pos - 1]) === true) {
  537. $typeHint = $realParams[$pos - 1]['type_hint'];
  538. if ($typeHint !== '') {
  539. $data = [$typeHint, $paramName, $pos];
  540. $this->helper->addMessage($commentEnd + 2, Helper::INVALID_TYPE_HINT, $data);
  541. }
  542. }
  543. }
  544. }
  545. // Make sure the names of the parameter comment matches the
  546. // actual parameter.
  547. if (isset($realParams[$pos - 1]) === true) {
  548. $realName = $realParams[$pos - 1]['name'];
  549. $foundParams[] = $realName;
  550. // Append ampersand to name if passing by reference.
  551. if ($realParams[$pos - 1]['pass_by_reference'] === true) {
  552. $realName = '&' . $realName;
  553. }
  554. if ($realName !== $paramName) {
  555. $code = Helper::PARAM_NAME_NO_MATCH;
  556. $data = [$paramName, $realName, $pos];
  557. if (strtolower($paramName) === strtolower($realName)) {
  558. $code = Helper::PARAM_NAME_NO_CASE_MATCH;
  559. }
  560. $this->helper->addMessage($errorPos, $code, $data);
  561. }
  562. } elseif (substr($paramName, -4) !== ',...') {
  563. // We must have an extra parameter comment.
  564. $this->helper->addMessage($errorPos, Helper::EXTRA_PARAM_COMMENT, [$pos]);
  565. }
  566. if ($param->getVarName() === '') {
  567. $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_NAME, [$pos]);
  568. }
  569. if ($param->getType() === '') {
  570. $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_TYPE, [$pos]);
  571. }
  572. if ($paramComment === '') {
  573. $data = [$paramName, $pos];
  574. $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_COMMENT, $data);
  575. } else {
  576. // Param comments must start with a capital letter and
  577. // end with the full stop.
  578. $firstChar = $paramComment[0];
  579. if (preg_match('|\p{Lu}|u', $firstChar) === 0) {
  580. $this->helper->addMessage($errorPos, Helper::PARAM_COMMENT_NOT_CAPITAL);
  581. }
  582. $lastChar = $paramComment[strlen($paramComment) - 1];
  583. if ($lastChar !== '.') {
  584. $this->helper->addMessage($errorPos, Helper::PARAM_COMMENT_FULL_STOP);
  585. }
  586. }
  587. $previousParam = $param;
  588. }
  589. if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
  590. $this->helper->addMessage($longestType, Helper::SPACING_AFTER_LONG_TYPE);
  591. }
  592. if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
  593. $this->helper->addMessage($longestVar, Helper::SPACING_AFTER_LONG_NAME);
  594. }
  595. }
  596. $realNames = [];
  597. foreach ($realParams as $realParam) {
  598. $realNames[] = $realParam['name'];
  599. }
  600. // Report missing comments.
  601. $diff = array_diff($realNames, $foundParams);
  602. foreach ($diff as $neededParam) {
  603. if (count($params) !== 0) {
  604. $errorPos = $params[count($params) - 1]->getLine() + $commentStart;
  605. } else {
  606. $errorPos = $commentStart;
  607. }
  608. $data = [$neededParam];
  609. $this->helper->addMessage($errorPos, Helper::MISSING_PARAM_TAG, $data);
  610. }
  611. }
  612. }