PageRenderTime 57ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/.dev/code-sniffs/XLite/TagsSniff.php

https://github.com/istran/core
PHP | 1012 lines | 690 code | 153 blank | 169 comment | 120 complexity | 81564842091e4fb15326425ebfcda415 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * @version $Id: f064ec1530e3cae9c4e18e0eecc2a703ad7a2bbe $
  4. */
  5. class XLite_TagsSniff extends XLite_ReqCodesSniff implements PHP_CodeSniffer_Sniff
  6. {
  7. /**
  8. * The name of the method that we are currently processing.
  9. *
  10. * @var string
  11. */
  12. protected $_methodName = '';
  13. /**
  14. * The position in the stack where the fucntion token was found.
  15. *
  16. * @var int
  17. */
  18. protected $_functionToken = null;
  19. /**
  20. * The position in the stack where the class token was found.
  21. *
  22. * @var int
  23. */
  24. protected $_classToken = null;
  25. /**
  26. * The header comment parser for the current file.
  27. *
  28. * @var PHP_CodeSniffer_Comment_Parser_ClassCommentParser
  29. */
  30. protected $commentParser = null;
  31. /**
  32. * The current PHP_CodeSniffer_File object we are processing.
  33. *
  34. * @var PHP_CodeSniffer_File
  35. */
  36. protected $currentFile = null;
  37. /**
  38. * Tags in correct order and related info.
  39. *
  40. * @var array
  41. */
  42. protected $tags = array();
  43. protected $reqCodesWrongFormat = array(
  44. 'category' => array(
  45. 'code' => 'REQ.PHP.4.1.11',
  46. 'function' => 'getCategory',
  47. 'type' => 'single',
  48. ),
  49. 'package' => array(
  50. 'code' => 'REQ.PHP.4.1.11',
  51. 'function' => 'getPackage',
  52. 'type' => 'single',
  53. ),
  54. 'subpackage' => array(
  55. 'code' => 'REQ.PHP.4.1.11',
  56. 'function' => 'getSubpackage',
  57. 'type' => 'single',
  58. ),
  59. 'author' => array(
  60. 'code' => 'REQ.PHP.4.1.13',
  61. 'function' => 'getAuthors',
  62. 'type' => 'array',
  63. ),
  64. 'copyright' => array(
  65. 'code' => 'REQ.PHP.4.1.14',
  66. 'function' => 'getCopyrights',
  67. 'type' => 'array',
  68. ),
  69. 'license' => array(
  70. 'code' => 'REQ.PHP.4.1.15',
  71. 'function' => 'getLicense',
  72. 'type' => 'single',
  73. ),
  74. 'version' => array(
  75. 'code' => 'REQ.PHP.4.1.16',
  76. 'function' => 'getVersion',
  77. 'type' => 'single',
  78. ),
  79. 'param' => array(
  80. 'code' => 'REQ.PHP.4.1.26',
  81. 'function' => '',
  82. 'type' => '',
  83. ),
  84. );
  85. protected $reqCodePHPVersion = false;
  86. protected $reqCodeRequire = false;
  87. protected $reqCodeForbidden = false;
  88. protected $reqCodeOnlyOne = false;
  89. protected $reqCodeWrongOrder = 'REQ.PHP.4.1.9';
  90. protected $reqCodeUngroup = 'REQ.PHP.4.1.10';
  91. protected $reqCodeIndent = 'REQ.PHP.4.1.8';
  92. protected $reqCodeEmpty = 'REQ.PHP.4.1.12';
  93. protected $reqCodeDefault = 'REQ.PHP.4.1.20';
  94. protected $docBlock = 'unknown';
  95. protected $allowedParamTypes = array(
  96. 'integer', 'float', 'string', 'array', 'mixed', 'boolean', 'null', 'object', 'resource',
  97. );
  98. protected $allowedReturnTypes = array(
  99. 'integer', 'float', 'string', 'array', 'mixed', 'boolean', 'void', 'object', 'resource',
  100. );
  101. /**
  102. * Returns an array of tokens this test wants to listen for.
  103. *
  104. * @return array
  105. */
  106. public function register()
  107. {
  108. return array(T_OPEN_TAG);
  109. }//end register()
  110. /**
  111. * Processes this test, when one of its tokens is encountered.
  112. *
  113. * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
  114. * @param int $stackPtr The position of the current token
  115. * in the stack passed in $tokens.
  116. *
  117. * @return void
  118. */
  119. public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
  120. {
  121. }
  122. /**
  123. * Check that the PHP version is specified.
  124. *
  125. * @param int $commentStart Position in the stack where the comment started.
  126. * @param int $commentEnd Position in the stack where the comment ended.
  127. * @param string $comment The text of the function comment.
  128. *
  129. * @return void
  130. */
  131. protected function processPHPVersion($commentStart, $commentEnd, $commentText)
  132. {
  133. if ($this->reqCodePHPVersion && preg_match('/PHP version \d+\.\d+\.\d+$/Sm', $commentText) === false) {
  134. $error = 'PHP version not specified';
  135. $this->currentFile->addError($this->getReqPrefix($this->reqCodePHPVersion) . $error, $commentEnd);
  136. }
  137. }//end processPHPVersion()
  138. /**
  139. * Processes each required or optional tag.
  140. *
  141. * @param int $commentStart Position in the stack where the comment started.
  142. * @param int $commentEnd Position in the stack where the comment ended.
  143. *
  144. * @return void
  145. */
  146. protected function processTags($commentStart, $commentEnd)
  147. {
  148. $foundTags = $this->commentParser->getTagOrders();
  149. $orderIndex = 0;
  150. $indentation = array();
  151. $longestTag = 0;
  152. $errorPos = 0;
  153. $this->checkGotchas($commentStart, $commentEnd);
  154. $diff = array_diff($foundTags, array_keys($this->tags), array('comment'));
  155. if (count($diff) > 0 && $this->reqCodeForbidden) {
  156. foreach ($diff as $tag) {
  157. $error = "Forbidden @$tag tag in " . $this->docBlock ." comment";
  158. $this->currentFile->addError($this->getReqPrefix($this->reqCodeForbidden) . $error, $commentEnd);
  159. }
  160. }
  161. foreach ($this->tags as $tag => $info) {
  162. // Required tag missing.
  163. if ($info['required'] === true && in_array($tag, $foundTags) === false && $this->reqCodeRequire) {
  164. $error = "Missing @$tag tag in " .$this->docBlock . " comment";
  165. $this->currentFile->addError($this->getReqPrefix($this->reqCodeRequire) . $error, $commentEnd);
  166. continue;
  167. }
  168. // Get the line number for current tag.
  169. $tagName = ucfirst($tag);
  170. if ($info['allow_multiple'] === true) {
  171. $tagName .= 's';
  172. }
  173. $getMethod = 'get'.$tagName;
  174. if (!method_exists($this->commentParser, $getMethod) && $info['allow_multiple'] !== true) {
  175. $getMethod .= 's';
  176. }
  177. if (!method_exists($this->commentParser, $getMethod))
  178. continue;
  179. $tagElement = $this->commentParser->$getMethod();
  180. if (is_null($tagElement) === true || empty($tagElement) === true) {
  181. continue;
  182. }
  183. $tagElements = is_array($tagElement) ? $tagElement : array($tagElement);
  184. $errorPos = $commentStart;
  185. if (is_array($tagElement) === false) {
  186. $errorPos = ($commentStart + $tagElement->getLine());
  187. }
  188. // Get the tag order.
  189. $foundIndexes = array_keys($foundTags, $tag);
  190. if (count($foundIndexes) > 1) {
  191. // Multiple occurance not allowed.
  192. if ($info['allow_multiple'] === false) {
  193. if ($this->reqCodeOnlyOne) {
  194. $error = "Only 1 @$tag tag is allowed in a " . $this->docBlock ." comment";
  195. $this->currentFile->addError($this->getReqPrefix($this->reqCodeOnlyOne) . $error, $errorPos);
  196. }
  197. } else {
  198. // Make sure same tags are grouped together.
  199. $i = 0;
  200. $count = $foundIndexes[0];
  201. foreach ($foundIndexes as $index) {
  202. if ($index !== $count) {
  203. $errorPosIndex = ($errorPos + $tagElement[$i]->getLine());
  204. $error = "@$tag tags must be grouped together";
  205. $this->currentFile->addError($this->getReqPrefix($this->reqCodeUngroup) . $error, $errorPosIndex);
  206. }
  207. $i++;
  208. $count++;
  209. }
  210. }
  211. }//end if
  212. // Check tag order.
  213. if ($foundIndexes[0] > $orderIndex) {
  214. $orderIndex = $foundIndexes[0];
  215. } else {
  216. if (is_array($tagElement) === true && empty($tagElement) === false) {
  217. $errorPos += $tagElement[0]->getLine();
  218. }
  219. $orderText = $info['order_text'];
  220. $error = "The @$tag tag is in the wrong order; the tag $orderText";
  221. $this->currentFile->addError($this->getReqPrefix($this->reqCodeWrongOrder) . $error, $errorPos);
  222. }
  223. // Store the indentation for checking.
  224. $len = strlen($tag);
  225. if ($len > $longestTag) {
  226. $longestTag = $len;
  227. }
  228. foreach ($tagElements as $key => $element) {
  229. $indentation[] = array(
  230. 'tag' => $tag,
  231. 'space' => $this->getIndentation($tag, $element),
  232. 'line' => $element->getLine(),
  233. 'value' => $this->getTagValue($element),
  234. );
  235. }
  236. $method = 'process' . $tagName;
  237. if (method_exists($this, $method) === true) {
  238. // Process each tag if a method is defined.
  239. call_user_func(array($this, $method), $errorPos, $commentEnd, $tagElements);
  240. } else {
  241. foreach ($tagElements as $key => $element) {
  242. if (method_exists($element, 'process')) {
  243. $element->process(
  244. $this->currentFile,
  245. $commentStart,
  246. $this->docBlock
  247. );
  248. }
  249. }
  250. }
  251. }//end foreach
  252. foreach ($indentation as $indentInfo) {
  253. $this->checkForDefaultValue($indentInfo['value'], $indentInfo['tag'], $commentStart + $indentInfo['line']);
  254. if ($indentInfo['space'] !== 0
  255. && $indentInfo['space'] !== ($longestTag + 1)
  256. ) {
  257. $expected = (($longestTag - strlen($indentInfo['tag'])) + 1);
  258. $space = ($indentInfo['space'] - strlen($indentInfo['tag']));
  259. $error = "@$indentInfo[tag] tag comment indented incorrectly. ";
  260. $error .= "Expected $expected spaces but found $space.";
  261. $getTagMethod = isset($this->reqCodesWrongFormat[$indentInfo['tag']]) ? $this->reqCodesWrongFormat[$indentInfo['tag']]['function'] : false;
  262. $line = $indentInfo['line'];
  263. if ($this->tags[$indentInfo['tag']]['allow_multiple'] === true) {
  264. $line = $indentInfo['line'];
  265. } elseif ($getTagMethod && method_exists($this->commentParser, $getTagMethod)) {
  266. $tagElem = $this->commentParser->$getTagMethod();
  267. if ('array' === $this->reqCodesWrongFormat[$indentInfo['tag']]['type']) {
  268. $tagElem = array_pop($tagElem);
  269. }
  270. $line = $tagElem->getLine();
  271. }
  272. $this->currentFile->addError($this->getReqPrefix($this->reqCodeIndent) . $error, ($commentStart + $line));
  273. }
  274. }
  275. }//end processTags()
  276. protected function checkForDefaultValue($value, $tag, $line) {
  277. // REMOVE THIS LATER
  278. return;
  279. if (preg_match('/____\w+____/', $value)) {
  280. $error = 'Тег @' . $tag . ' имеет дефолтное значение. Его необходимо сменить';
  281. $this->currentFile->addError($this->getReqPrefix($this->reqCodeDefault) . $error, $line);
  282. return true;
  283. }
  284. return false;
  285. }
  286. protected function checkGotchas($commentStart, $commentEnd) {
  287. $gotchas = $this->getGotchas($commentStart, $commentEnd);
  288. $lastPos = $commentStart;
  289. foreach ($gotchas as $g) {
  290. if (!in_array($g['name'], array('TODO', 'FIXME', 'KLUDGE', 'TRICKY', 'WARNING', 'PARSER'))) {
  291. $this->currentFile->addError(
  292. $this->getReqPrefix('REQ.PHP.4.2.1') . 'При использовании gotchas необходимо использовать зарезервированные слова',
  293. $g['begin']
  294. );
  295. }
  296. if ($g['begin'] != $lastPos + 1) {
  297. $this->currentFile->addError(
  298. $this->getReqPrefix('REQ.PHP.4.2.2') . 'Ключевое слово gotcha должно ставиться в самом начале комментария',
  299. $g['begin']
  300. );
  301. }
  302. if ($g['link']['type'] && !in_array($g['link']['type'], array('M', 'C', 'T'))) {
  303. $this->currentFile->addError(
  304. $this->getReqPrefix('REQ.PHP.4.2.4', 'REQ.PHP.4.2.5', 'REQ.PHP.4.2.6') . 'Тип ссыли должен быть M или C или T',
  305. $g['begin']
  306. );
  307. }
  308. $lastPos = $g['end'];
  309. }
  310. }
  311. protected function getGotchas($commentStart, $commentEnd) {
  312. $tokens = $this->currentFile->getTokens();
  313. $gotchas = array();
  314. $idx = false;
  315. for ($i = $commentStart + 1; $i < $commentEnd - 2; $i++) {
  316. if (!preg_match('/\s+:([\w\d\_]+): (.+)$/S', $tokens[$i]['content'], $match)) {
  317. if ($idx !== false) {
  318. if (preg_match('/^[ ]* \*\s*$/S', $tokens[$i]['content'])) {
  319. $gotchas[$idx]['end'] = $i - 1;
  320. $idx = false;
  321. } elseif (preg_match('/^[ ]* \*(.+)$/S', $tokens[$i]['content'], $match)) {
  322. $gotchas[$idx]['text'] .= ' ' . trim($match[1]);
  323. }
  324. }
  325. continue;
  326. }
  327. $gotcha = array(
  328. 'name' => $match[1],
  329. 'text' => trim($match[2]),
  330. 'begin' => $i,
  331. 'end' => $i,
  332. 'link' => array(
  333. 'type' => false,
  334. 'id' => false
  335. )
  336. );
  337. if (preg_match('/^([\w]):([\d]+) /S', $gotcha['text'], $match)) {
  338. $gotcha['link']['type'] = $match[1];
  339. $gotcha['link']['id'] = $match[2];
  340. $gotcha['text'] = trim(substr($gotcha['text'], strlen($match[0])));
  341. }
  342. $idx = count($gotchas);
  343. $gotchas[$idx] = $gotcha;
  344. }
  345. return $gotchas;
  346. }
  347. /**
  348. * getTagValue
  349. *
  350. * @param string $tagElement The doc comment element
  351. *
  352. * @return void
  353. * @access protected
  354. */
  355. protected function getTagValue($tagElement, &$type = '')
  356. {
  357. if ($tagElement instanceof PHP_CodeSniffer_CommentParser_SingleElement) {
  358. $type = 'single';
  359. return $tagElement->getContent();
  360. } elseif ($tagElement instanceof PHP_CodeSniffer_CommentParser_PairElement) {
  361. $type = 'pair';
  362. return $tagElement->getValue() . ' ' . $tagElement->getComment();
  363. }
  364. return '';
  365. }
  366. /**
  367. * Get the indentation information of each tag.
  368. *
  369. * @param string $tagName The name of the
  370. * doc comment
  371. * element.
  372. * @param PHP_CodeSniffer_CommentParser_DocElement $tagElement The doc comment
  373. * element.
  374. *
  375. * @return void
  376. */
  377. protected function getIndentation($tagName, $tagElement)
  378. {
  379. $elementType = '';
  380. if ('' !== $this->getTagValue($tagElement, $elementType)) {
  381. $funcName = '';
  382. if ($elementType == 'single') {
  383. $funcName = 'getWhitespaceBeforeContent';
  384. } elseif ($elementType == 'pair') {
  385. $funcName = 'getWhitespaceBeforeValue';
  386. }
  387. if (!empty($funcName)) {
  388. return (strlen($tagName) + substr_count($tagElement->$funcName(), ' '));
  389. }
  390. }
  391. return 0;
  392. }//end getIndentation()
  393. /**
  394. * Process the category tag.
  395. *
  396. * @param int $errorPos The line number where the error occurs.
  397. *
  398. * @return void
  399. */
  400. protected function processCategory($errorPos)
  401. {
  402. $tag = $this->commentParser->getCategory();
  403. if ($tag !== null) {
  404. $content = $tag->getContent();
  405. if ($content !== '') {
  406. list($isValid, $validName) = $this->checkCategory($content);
  407. if (!$isValid) {
  408. $error = "Category name \"$content\" is not valid; consider \"$validName\" instead";
  409. $this->currentFile->addError($this->getReqPrefix($this->getReqCode($this->reqCodesWrongFormat, 'category')) . $error, $errorPos);
  410. }
  411. } else {
  412. $error = '@category tag must contain a name';
  413. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . $error, $errorPos);
  414. }
  415. }
  416. }
  417. /**
  418. * check category tag
  419. *
  420. * @param string $content Tag content
  421. * @access protected
  422. * @return array
  423. * @since 1.0.0
  424. */
  425. protected function checkCategory($content)
  426. {
  427. $result = array(true, $content);
  428. if (PHP_CodeSniffer::isUnderscoreName($content) !== true) {
  429. $result = array(false, $this->sanitazeUnderscoreName($content));
  430. }
  431. return $result;
  432. }
  433. /**
  434. * Process the package tag.
  435. *
  436. * @param int $errorPos The line number where the error occurs.
  437. *
  438. * @return void
  439. */
  440. protected function processPackage($errorPos)
  441. {
  442. $tag = $this->commentParser->getPackage();
  443. if ($tag !== null) {
  444. $content = $tag->getContent();
  445. if ($content !== '') {
  446. list($isValid, $validName) = $this->checkPackage($content);
  447. if (!$isValid) {
  448. $error = "Package name \"$content\" is not valid; consider \"$validName\" instead";
  449. $this->currentFile->addError($this->getReqPrefix($this->getReqCode($this->reqCodesWrongFormat, 'package')) . $error, $errorPos);
  450. }
  451. } else {
  452. $error = '@package tag must contain a name';
  453. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . $error, $errorPos);
  454. }
  455. }
  456. }
  457. /**
  458. * check package tag
  459. *
  460. * @param string $content Tag content
  461. * @access protected
  462. * @return array
  463. * @since 1.0.0
  464. */
  465. protected function checkPackage($content)
  466. {
  467. $result = array(true, $content);
  468. if (PHP_CodeSniffer::isUnderscoreName($content) !== true) {
  469. $result = array(false, $this->sanitazeUnderscoreName($content));
  470. }
  471. return $result;
  472. }
  473. /**
  474. * Process the subpackage tag.
  475. *
  476. * @param int $errorPos The line number where the error occurs.
  477. *
  478. * @return void
  479. */
  480. protected function processSubpackage($errorPos)
  481. {
  482. $tag = $this->commentParser->getSubpackage();
  483. if ($tag !== null) {
  484. $content = $tag->getContent();
  485. if ($content !== '') {
  486. list($isValid, $validName) = $this->checkSubpackage($content);
  487. if (!$isValid) {
  488. $error = "Subpackage name \"$content\" is not valid; consider \"$validName\" instead";
  489. $this->currentFile->addError($this->getReqPrefix($this->getReqCode($this->reqCodesWrongFormat, 'subpackage')) . $error, $errorPos);
  490. }
  491. } else {
  492. $error = '@subpackage tag must contain a name';
  493. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . $error, $errorPos);
  494. }
  495. }
  496. }
  497. /**
  498. * check subpackage tag
  499. *
  500. * @param string $content Tag content
  501. * @access protected
  502. * @return array
  503. * @since 1.0.0
  504. */
  505. protected function checkSubpackage($content)
  506. {
  507. $result = array(true, $content);
  508. if (PHP_CodeSniffer::isUnderscoreName($content) !== true) {
  509. $result = array(false, $this->sanitazeUnderscoreName($content));
  510. }
  511. return $result;
  512. }
  513. protected function processAuthors($commentStart)
  514. {
  515. $authors = $this->commentParser->getAuthors();
  516. // Report missing return.
  517. if (empty($authors) === false) {
  518. foreach ($authors as $author) {
  519. $errorPos = ($commentStart + $author->getLine());
  520. $content = $author->getContent();
  521. if ($content !== 'Ruslan R. Fazliev <rrf@x-cart.com>') {
  522. $error = 'Content of the @author tag must be in the form "Ruslan R. Fazliev <rrf@x-cart.com>"';
  523. $this->currentFile->addError($this->getReqPrefix($this->getReqCode($this->reqCodesWrongFormat, 'author')) . $error, $errorPos);
  524. } else {
  525. $error = "Content missing for @author tag in " . $this->docBlock ." comment";
  526. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . ' ' . $error, $errorPos);
  527. }
  528. }
  529. }
  530. }//end processAuthors()
  531. protected function processCopyrights($commentStart)
  532. {
  533. $copyrights = $this->commentParser->getCopyrights();
  534. foreach ($copyrights as $copyright) {
  535. $errorPos = ($commentStart + $copyright->getLine());
  536. $content = $copyright->getContent();
  537. if (empty($content)) {
  538. $bYear = '2009';
  539. $eYear = date('Y');
  540. $text = 'Copyright (c) ' . (($bYear == $eYear) ? $bYear : $bYear . '-' . $eYear) . ' Ruslan R. Fazliev <rrf@x-cart.com>';
  541. if ($content !== $text) {
  542. $error = 'Content of the @copyright tag must be in the form "' . $text . '"';
  543. $this->currentFile->addError($this->getReqPrefix($this->getReqCode($this->reqCodesWrongFormat, 'copyright')) . $error, $errorPos);
  544. }//end if
  545. } else {
  546. $error = "Content missing for @copyright tag in " . $this->docBlock ." comment";
  547. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . ' ' . $error, $errorPos);
  548. }
  549. }//end if
  550. }//end processCopyrights()
  551. protected function processLicense($errorPos)
  552. {
  553. $license = $this->commentParser->getLicense();
  554. if ($license !== null) {
  555. $value = $license->getValue();
  556. $comment = $license->getComment();
  557. $content = $value . ' ' . $comment;
  558. if (empty($content)) {
  559. $error = "Content missing for @license tag in " . $this->docBlock ." comment";
  560. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . ' ' . $error, $errorPos);
  561. } elseif ($content !== 'http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)') {
  562. $error = 'Content of the @license tag must be in the form "http://www.qtmsoft.com/xpayments_eula.html X-Payments license agreement"';
  563. $this->currentFile->addError($this->getReqPrefix($this->getReqCode($this->reqCodesWrongFormat, 'license')) . $error, $errorPos);
  564. }
  565. }
  566. }//end processLicense()
  567. protected function processVersion($errorPos)
  568. {
  569. $version = $this->commentParser->getVersion();
  570. if ($version !== null) {
  571. $content = $version->getContent();
  572. $matches = array();
  573. if (empty($content) === true) {
  574. $error = 'Content missing for @version tag in file comment';
  575. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . $error, $errorPos);
  576. } else if (!preg_match('/^SVN: \$' . 'Id: [\w\d_\.]+ \d+ \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}Z [\w\d_]+ \$$/Ss', $content)) {
  577. $error = "Invalid version \"$content\" in file comment; consider \"SVN: <svn_id>\" instead";
  578. $this->currentFile->addWarning($this->getReqPrefix($this->getReqCode($this->reqCodesWrongFormat, 'version')) . $error, $errorPos);
  579. }
  580. }
  581. }//end processVersion()
  582. protected function processThrows($errorPos)
  583. {
  584. if (count($this->commentParser->getThrows()) === 0) {
  585. return;
  586. }
  587. foreach ($this->commentParser->getThrows() as $throw) {
  588. $exception = $throw->getValue();
  589. $errorPos = ($commentStart + $throw->getLine());
  590. if ($exception === '') {
  591. $error = '@throws tag must contain the exception class name';
  592. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . $error, $errorPos);
  593. }
  594. }
  595. }
  596. /**
  597. * Process the return comment of this function comment.
  598. *
  599. * @param int $commentStart The position in the stack where the comment started.
  600. * @param int $commentEnd The position in the stack where the comment ended.
  601. *
  602. * @return void
  603. */
  604. protected function processReturn($commentStart, $commentEnd)
  605. {
  606. // Skip constructor and destructor.
  607. $className = '';
  608. if ($this->_classToken !== null) {
  609. $className = $this->currentFile->getDeclarationName($this->_classToken);
  610. $className = strtolower(ltrim($className, '_'));
  611. }
  612. $methodName = strtolower(ltrim($this->_methodName, '_'));
  613. $isSpecialMethod = ($this->_methodName === '__construct' || $this->_methodName === '__destruct');
  614. // Check type
  615. if ($this->commentParser->getReturn()) {
  616. $r = $this->checkType($this->commentParser->getReturn()->getValue(), $this->allowedReturnTypes, 'return');
  617. if (true !== $r) {
  618. $this->currentFile->addError($this->getReqPrefix('?') . $r, $commentStart + $this->commentParser->getReturn()->getLine());
  619. }
  620. }
  621. // Check comment case
  622. if (
  623. $this->commentParser->getReturn()->getComment()
  624. && preg_match('/^[a-z]/Ss', trim($this->commentParser->getReturn()->getComment()))
  625. ) {
  626. $error = 'Комментарий аннотации возврата метода начинается с маленькой буквы';
  627. $this->currentFile->addError($this->getReqPrefix('?') . $error, $commentStart + $this->commentParser->getReturn()->getLine());
  628. }
  629. if ($isSpecialMethod === false && $methodName !== $className) {
  630. // Report missing return tag.
  631. if ($this->commentParser->getReturn() === null) {
  632. $error = 'Missing @return tag in function comment';
  633. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . $error, $commentEnd);
  634. } else if (trim($this->commentParser->getReturn()->getRawContent()) === '') {
  635. $error = '@return tag is empty in function comment';
  636. $errorPos = ($commentStart + $this->commentParser->getReturn()->getLine());
  637. $this->currentFile->addError($this->getReqPrefix($this->reqCodeEmpty) . $error, $errorPos);
  638. }
  639. }
  640. }//end processReturn()
  641. /**
  642. * Process the function parameter comments.
  643. *
  644. * @param int $commentStart The position in the stack where
  645. * the comment started.
  646. *
  647. * @return void
  648. */
  649. protected function processParams($commentStart, $commentEnd, $tagElements)
  650. {
  651. $realParams = $this->currentFile->getMethodParameters($this->_functionToken);
  652. $params = $this->commentParser->getParams();
  653. $foundParams = array();
  654. if (empty($params) === false) {
  655. $lastParm = (count($params) - 1);
  656. if (substr_count($params[$lastParm]->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) {
  657. $error = 'Last parameter comment requires a blank newline after it';
  658. $errorPos = ($params[$lastParm]->getLine() + $commentStart);
  659. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.20') . $error, $errorPos);
  660. }
  661. // Parameters must appear immediately after the comment.
  662. if ($params[0]->getOrder() !== 2) {
  663. $error = 'Parameters must appear immediately after the comment';
  664. $errorPos = ($params[0]->getLine() + $commentStart);
  665. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.21') . $error, $errorPos);
  666. }
  667. $previousParam = null;
  668. $spaceBeforeVar = 10000;
  669. $spaceBeforeComment = 10000;
  670. $longestType = 0;
  671. $longestVar = 0;
  672. foreach ($params as $param) {
  673. $paramComment = trim($param->getComment());
  674. $errorPos = ($param->getLine() + $commentStart);
  675. // Check type
  676. $r = $this->checkType($param->getType(), $this->allowedParamTypes, 'param');
  677. if (true !== $r) {
  678. $this->currentFile->addError($this->getReqPrefix('?') . $r, $errorPos);
  679. }
  680. // Make sure that there is only one space before the var type.
  681. if ($param->getWhitespaceBeforeType() !== ' ') {
  682. $error = 'Expected 1 space before variable type';
  683. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.22') . $error, $errorPos);
  684. }
  685. $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
  686. if ($spaceCount < $spaceBeforeVar) {
  687. $spaceBeforeVar = $spaceCount;
  688. $longestType = $errorPos;
  689. }
  690. $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
  691. if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
  692. $spaceBeforeComment = $spaceCount;
  693. $longestVar = $errorPos;
  694. }
  695. // Make sure they are in the correct order,
  696. // and have the correct name.
  697. $pos = $param->getPosition();
  698. $paramName = ($param->getVarName() !== '') ? $param->getVarName() : '[ UNKNOWN ]';
  699. if ($previousParam !== null) {
  700. $previousName = ($previousParam->getVarName() !== '') ? $previousParam->getVarName() : 'UNKNOWN';
  701. // Check to see if the parameters align properly.
  702. if ($param->alignsVariableWith($previousParam) === false) {
  703. $error = 'The variable names for parameters '.$previousName.' ('.($pos - 1).') and '.$paramName.' ('.$pos.') do not align';
  704. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.23') . $error, $errorPos);
  705. }
  706. if ($param->alignsCommentWith($previousParam) === false) {
  707. $error = 'The comments for parameters '.$previousName.' ('.($pos - 1).') and '.$paramName.' ('.$pos.') do not align';
  708. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.24') . $error, $errorPos);
  709. }
  710. }//end if
  711. // Make sure the names of the parameter comment matches the
  712. // actual parameter.
  713. if (isset($realParams[($pos - 1)]) === true) {
  714. $realName = $realParams[($pos - 1)]['name'];
  715. $foundParams[] = $realName;
  716. // Append ampersand to name if passing by reference.
  717. if ($realParams[($pos - 1)]['pass_by_reference'] === true) {
  718. $realName = '&'.$realName;
  719. }
  720. if ($realName !== $param->getVarName()) {
  721. $error = 'Doc comment var "'.$paramName;
  722. $error .= '" does not match actual variable name "'.$realName;
  723. $error .= '" at position '.$pos;
  724. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.25') . $error, $errorPos);
  725. }
  726. } else {
  727. // We must have an extra parameter comment.
  728. $error = 'Superfluous doc comment at position '.$pos;
  729. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.27') . $error, $errorPos);
  730. }
  731. if ($param->getVarName() === '') {
  732. $error = 'Missing parameter name at position '.$pos;
  733. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.26') . $error, $errorPos);
  734. }
  735. if ($param->getType() === '') {
  736. $error = 'Missing type at position '.$pos;
  737. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.26') . $error, $errorPos);
  738. }
  739. if ($paramComment === '') {
  740. $error = 'Missing comment for param "'.$paramName.'" at position '.$pos;
  741. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.26') . $error, $errorPos);
  742. } elseif (preg_match('/^[a-z]/Ss', trim($paramComment))) {
  743. $error = 'Комментарий параметра "' . $paramName . '" начинается с маленькой буквы';
  744. $this->currentFile->addError($this->getReqPrefix('?') . $error, $errorPos);
  745. }
  746. $this->checkForDefaultValue($paramName, 'param', $errorPos);
  747. $this->checkForDefaultValue($paramComment, 'param', $errorPos);
  748. $previousParam = $param;
  749. }//end foreach
  750. if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
  751. $error = 'Expected 1 space after the longest type';
  752. $this->currentFile->addError($this->getReqPrefix('?') . $error, $longestType);
  753. }
  754. if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
  755. $error = 'Expected 1 space after the longest variable name';
  756. $this->currentFile->addError($this->getReqPrefix('?') . $error, $longestVar);
  757. }
  758. }//end if
  759. $realNames = array();
  760. foreach ($realParams as $realParam) {
  761. $realNames[] = $realParam['name'];
  762. }
  763. // Report and missing comments.
  764. $diff = array_diff($realNames, $foundParams);
  765. foreach ($diff as $neededParam) {
  766. if (count($params) !== 0) {
  767. $errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
  768. } else {
  769. $errorPos = $commentStart;
  770. }
  771. $error = 'Doc comment for "'.$neededParam.'" missing';
  772. $this->currentFile->addError($this->getReqPrefix('REQ.PHP.4.1.27') . $error, $errorPos);
  773. }
  774. }//end processParams()
  775. protected function checkType($rawType, array $allowedTypes, $tag)
  776. {
  777. $types = array_map('trim', explode('|', $rawType));
  778. if (4 < count($types)) {
  779. $this->currentFile->addError($this->getReqPrefix('?') . 'Число вариантов типов @' . $tag . ' больше 4', $errorPos);
  780. }
  781. $result = true;
  782. foreach ($types as $type) {
  783. if ('\\' == substr($type, 0, 1) || in_array($type, $allowedTypes)) {
  784. // Class or simple type
  785. continue;
  786. } elseif (preg_match('/^array\((.+)\)$/Ss', $type, $m)) {
  787. // Array
  788. $r = $this->checkType($m[1], $allowedTypes, $tag);
  789. if (true === $r) {
  790. continue;
  791. }
  792. $result = $r;
  793. } else {
  794. $result = 'Тип "' . $type . '" запрещен для использования в @' . $tag;
  795. }
  796. break;
  797. }
  798. return $result;
  799. }
  800. function checkAccess($stackPtr, $commentStart, $commentEnd) {
  801. $tokens = $this->currentFile->getTokens();
  802. $access = $this->commentParser->getAccess();
  803. $prevWS = $this->currentFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, false, "\n");
  804. $type = $this->currentFile->findNext(array(T_PRIVATE, T_PUBLIC, T_PROTECTED), $prevWS + 1, $stackPtr - 1);
  805. $code = $tokens[$type]['code'];
  806. if (
  807. !is_null($access)
  808. && (
  809. ($code === T_PUBLIC && $access->getValue() !== 'public')
  810. || ($code === T_PRIVATE && $access->getValue() !== 'private')
  811. || (($code === T_PROTECTED && $access->getValue() !== 'protected'))
  812. )
  813. ) {
  814. $cnt = substr_count(
  815. preg_replace('/@access.+$/Ss', '', $this->currentFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1))),
  816. "\n"
  817. );
  818. $this->currentFile->addError(
  819. $this->getReqPrefix('REQ.PHP.4.1.25') . 'Значение тэга @access не совпадает с декларацией (декларированно как ' . $tokens[$type]['content']. ', а @access равен ' . $access->getValue() . ')',
  820. $commentStart + $cnt
  821. );
  822. }
  823. }
  824. /**
  825. * Service function
  826. */
  827. /**
  828. * sanitaze underscore name (Pascal Case + underscore as word delimiter)
  829. *
  830. * @param string $content
  831. * @access private
  832. * @return string
  833. * @since 1.0.0
  834. */
  835. private function sanitazeUnderscoreName($content)
  836. {
  837. $newContent = str_replace(' ', '_', $content);
  838. $nameBits = explode('_', $newContent);
  839. $firstBit = array_shift($nameBits);
  840. $newName = ucfirst($firstBit).'_';
  841. foreach ($nameBits as $bit) {
  842. $newName .= ucfirst($bit).'_';
  843. }
  844. return trim($newName, '_');
  845. }
  846. }