PageRenderTime 173ms CodeModel.GetById 61ms app.highlight 65ms RepoModel.GetById 35ms app.codeStats 1ms

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