* @author Marc McIntyre * @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600) * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence * @link http://pear.php.net/package/PHP_CodeSniffer */ if (class_exists('PHP_CodeSniffer_CommentParser_SingleElement', true) === false) { $error = 'Class PHP_CodeSniffer_CommentParser_SingleElement not found'; throw new PHP_CodeSniffer_Exception($error); } if (class_exists('PHP_CodeSniffer_CommentParser_CommentElement', true) === false) { $error = 'Class PHP_CodeSniffer_CommentParser_CommentElement not found'; throw new PHP_CodeSniffer_Exception($error); } if (class_exists('PHP_CodeSniffer_CommentParser_ParserException', true) === false) { $error = 'Class PHP_CodeSniffer_CommentParser_ParserException not found'; throw new PHP_CodeSniffer_Exception($error); } /** * Parses doc comments. * * This abstract parser handles the following tags: * * * * Extending classes should implement the getAllowedTags() method to return the * tags that they wish to process, ommiting the tags that this base class * processes. When one of these tags in encountered, the process<tag_name> * method is called on that class. For example, if a parser's getAllowedTags() * method returns \@param as one of its tags, the processParam method will be * called so that the parser can process such a tag. * * The method is passed the tokens that comprise this tag. The tokens array * includes the whitespace that exists between the tokens, as seperate tokens. * It's up to the method to create a element that implements the DocElement * interface, which should be returned. The AbstractDocElement class is a helper * class that can be used to handle most of the parsing of the tokens into their * individual sub elements. It requires that you construct it with the element * previous to the element currently being processed, which can be acquired * with the protected $previousElement class member of this class. * * @category PHP * @package PHP_CodeSniffer * @author Greg Sherwood * @author Marc McIntyre * @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600) * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence * @version Release: 1.3.3 * @link http://pear.php.net/package/PHP_CodeSniffer */ abstract class PHP_CodeSniffer_CommentParser_AbstractParser { /** * The comment element that appears in the doc comment. * * @var PHP_CodeSniffer_CommentParser_CommentElement */ protected $comment = null; /** * The string content of the comment. * * @var string */ protected $commentString = ''; /** * The file that the comment exists in. * * @var PHP_CodeSniffer_File */ protected $phpcsFile = null; /** * The word tokens that appear in the comment. * * Whitespace tokens also appear in this stack, but are separate tokens * from words. * * @var array(string) */ protected $words = array(); /** * An array of all tags found in the comment. * * @var array(string) */ protected $foundTags = array(); /** * The previous doc element that was processed. * * null if the current element being processed is the first element in the * doc comment. * * @var PHP_CodeSniffer_CommentParser_DocElement */ protected $previousElement = null; /** * A list of see elements that appear in this doc comment. * * @var array(PHP_CodeSniffer_CommentParser_SingleElement) */ protected $sees = array(); /** * A list of see elements that appear in this doc comment. * * @var array(PHP_CodeSniffer_CommentParser_SingleElement) */ protected $deprecated = null; /** * A list of see elements that appear in this doc comment. * * @var array(PHP_CodeSniffer_CommentParser_SingleElement) */ protected $links = array(); /** * A element to represent \@since tags. * * @var PHP_CodeSniffer_CommentParser_SingleElement */ protected $since = null; /** * True if the comment has been parsed. * * @var boolean */ private $_hasParsed = false; /** * The tags that this class can process. * * @var array(string) */ private static $_tags = array( 'see' => false, 'link' => false, 'deprecated' => true, 'since' => true, ); /** * An array of unknown tags. * * @var array(string) */ public $unknown = array(); /** * The order of tags. * * @var array(string) */ public $orders = array(); /** * Constructs a Doc Comment Parser. * * @param string $comment The comment to parse. * @param PHP_CodeSniffer_File $phpcsFile The file that this comment is in. */ public function __construct($comment, PHP_CodeSniffer_File $phpcsFile) { $this->commentString = $comment; $this->phpcsFile = $phpcsFile; }//end __construct() /** * Initiates the parsing of the doc comment. * * @return void * @throws PHP_CodeSniffer_CommentParser_ParserException If the parser finds a * problem with the * comment. */ public function parse() { if ($this->_hasParsed === false) { $this->_parse($this->commentString); } }//end parse() /** * Parse the comment. * * @param string $comment The doc comment to parse. * * @return void * @see _parseWords() */ private function _parse($comment) { // Firstly, remove the comment tags and any stars from the left side. $lines = explode($this->phpcsFile->eolChar, $comment); foreach ($lines as &$line) { $line = trim($line); if ($line !== '') { if (substr($line, 0, 3) === '/**') { $line = substr($line, 3); } else if (substr($line, -2, 2) === '*/') { $line = substr($line, 0, -2); } else if ($line{0} === '*') { $line = substr($line, 1); } // Add the words to the stack, preserving newlines. Other parsers // might be interested in the spaces between words, so tokenize // spaces as well as separate tokens. $flags = (PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $words = preg_split( '|(\s+)|', $line.$this->phpcsFile->eolChar, -1, $flags ); $this->words = array_merge($this->words, $words); }//end if }//end foreach $this->_parseWords(); }//end _parse() /** * Parses each word within the doc comment. * * @return void * @see _parse() * @throws PHP_CodeSniffer_CommentParser_ParserException If more than the allowed * number of occurances of * a tag is found. */ private function _parseWords() { $allowedTags = (self::$_tags + $this->getAllowedTags()); $allowedTagNames = array_keys($allowedTags); $prevTagPos = false; $wordWasEmpty = true; foreach ($this->words as $wordPos => $word) { if (trim($word) !== '') { $wordWasEmpty = false; } if ($word{0} === '@') { $tag = substr($word, 1); // Filter out @ tags in the comment description. // A real comment tag should have whitespace and a newline before it. if (isset($this->words[($wordPos - 1)]) === false || trim($this->words[($wordPos - 1)]) !== '' ) { continue; } if (isset($this->words[($wordPos - 2)]) === false || $this->words[($wordPos - 2)] !== $this->phpcsFile->eolChar ) { continue; } $this->foundTags[] = array( 'tag' => $tag, 'line' => $this->getLine($wordPos), 'pos' => $wordPos, ); if ($prevTagPos !== false) { // There was a tag before this so let's process it. $prevTag = substr($this->words[$prevTagPos], 1); $this->parseTag($prevTag, $prevTagPos, ($wordPos - 1)); } else { // There must have been a comment before this tag, so // let's process that. $this->parseTag('comment', 0, ($wordPos - 1)); } $prevTagPos = $wordPos; if (in_array($tag, $allowedTagNames) === false) { // This is not a tag that we process, but let's check to // see if it is a tag we know about. If we don't know about it, // we add it to a list of unknown tags. $knownTags = array( 'abstract', 'access', 'example', 'filesource', 'global', 'ignore', 'internal', 'name', 'static', 'staticvar', 'todo', 'tutorial', 'uses', 'package_version@', ); if (in_array($tag, $knownTags) === false) { $this->unknown[] = array( 'tag' => $tag, 'line' => $this->getLine($wordPos), 'pos' => $wordPos, ); } }//end if }//end if }//end foreach // Only process this tag if there was something to process. if ($wordWasEmpty === false) { if ($prevTagPos === false) { // There must only be a comment in this doc comment. $this->parseTag('comment', 0, count($this->words)); } else { // Process the last tag element. $prevTag = substr($this->words[$prevTagPos], 1); $numWords = count($this->words); $endPos = $numWords; if ($prevTag === 'package' || $prevTag === 'subpackage') { // These are single-word tags, so anything after a newline // is really a comment. for ($endPos = $prevTagPos; $endPos < $numWords; $endPos++) { if (strpos($this->words[$endPos], $this->phpcsFile->eolChar) !== false) { break; } } } $this->parseTag($prevTag, $prevTagPos, $endPos); if ($endPos !== $numWords) { // Process the final comment, if it is not empty. $tokens = array_slice($this->words, ($endPos + 1), $numWords); $content = implode('', $tokens); if (trim($content) !== '') { $this->parseTag('comment', ($endPos + 1), $numWords); } } }//end if }//end if }//end _parseWords() /** * Returns the line that the token exists on in the doc comment. * * @param int $tokenPos The position in the words stack to find the line * number for. * * @return int */ protected function getLine($tokenPos) { $newlines = 0; for ($i = 0; $i < $tokenPos; $i++) { $newlines += substr_count($this->phpcsFile->eolChar, $this->words[$i]); } return $newlines; }//end getLine() /** * Parses see tag element within the doc comment. * * @param array(string) $tokens The word tokens that comprise this element. * * @return DocElement The element that represents this see comment. */ protected function parseSee($tokens) { $see = new PHP_CodeSniffer_CommentParser_SingleElement( $this->previousElement, $tokens, 'see', $this->phpcsFile ); $this->sees[] = $see; return $see; }//end parseSee() /** * Parses the comment element that appears at the top of the doc comment. * * @param array(string) $tokens The word tokens that comprise tihs element. * * @return DocElement The element that represents this comment element. */ protected function parseComment($tokens) { $this->comment = new PHP_CodeSniffer_CommentParser_CommentElement( $this->previousElement, $tokens, $this->phpcsFile ); return $this->comment; }//end parseComment() /** * Parses \@deprecated tags. * * @param array(string) $tokens The word tokens that comprise tihs element. * * @return DocElement The element that represents this deprecated tag. */ protected function parseDeprecated($tokens) { $this->deprecated = new PHP_CodeSniffer_CommentParser_SingleElement( $this->previousElement, $tokens, 'deprecated', $this->phpcsFile ); return $this->deprecated; }//end parseDeprecated() /** * Parses \@since tags. * * @param array(string) $tokens The word tokens that comprise this element. * * @return SingleElement The element that represents this since tag. */ protected function parseSince($tokens) { $this->since = new PHP_CodeSniffer_CommentParser_SingleElement( $this->previousElement, $tokens, 'since', $this->phpcsFile ); return $this->since; }//end parseSince() /** * Parses \@link tags. * * @param array(string) $tokens The word tokens that comprise this element. * * @return SingleElement The element that represents this link tag. */ protected function parseLink($tokens) { $link = new PHP_CodeSniffer_CommentParser_SingleElement( $this->previousElement, $tokens, 'link', $this->phpcsFile ); $this->links[] = $link; return $link; }//end parseLink() /** * Returns the see elements that appear in this doc comment. * * @return array(SingleElement) */ public function getSees() { return $this->sees; }//end getSees() /** * Returns the comment element that appears at the top of this doc comment. * * @return CommentElement */ public function getComment() { return $this->comment; }//end getComment() /** * Returns the word list. * * @return array */ public function getWords() { return $this->words; }//end getWords() /** * Returns the list of found tags. * * @return array */ public function getTags() { return $this->foundTags; }//end getTags() /** * Returns the link elements found in this comment. * * Returns an empty array if no links are found in the comment. * * @return array(SingleElement) */ public function getLinks() { return $this->links; }//end getLinks() /** * Returns the deprecated element found in this comment. * * Returns null if no element exists in the comment. * * @return SingleElement */ public function getDeprecated() { return $this->deprecated; }//end getDeprecated() /** * Returns the since element found in this comment. * * Returns null if no element exists in the comment. * * @return SingleElement */ public function getSince() { return $this->since; }//end getSince() /** * Parses the specified tag. * * @param string $tag The tag name to parse (omitting the @ sybmol from * the tag) * @param int $start The position in the word tokens where this element * started. * @param int $end The position in the word tokens where this element * ended. * * @return void * @throws Exception If the process method for the tag cannot be found. */ protected function parseTag($tag, $start, $end) { $tokens = array_slice($this->words, ($start + 1), ($end - $start)); $allowedTags = (self::$_tags + $this->getAllowedTags()); $allowedTagNames = array_keys($allowedTags); if ($tag === 'comment' || in_array($tag, $allowedTagNames) === true) { $method = 'parse'.$tag; if (method_exists($this, $method) === false) { $error = 'Method '.$method.' must be implemented to process '.$tag.' tags'; throw new Exception($error); } $this->previousElement = $this->$method($tokens); } else { $this->previousElement = new PHP_CodeSniffer_CommentParser_SingleElement( $this->previousElement, $tokens, $tag, $this->phpcsFile ); } $this->orders[] = $tag; if ($this->previousElement === null || ($this->previousElement instanceof PHP_CodeSniffer_CommentParser_DocElement) === false ) { throw new Exception('Parse method must return a DocElement'); } }//end parseTag() /** * Returns a list of tags that this comment parser allows for it's comment. * * Each tag should indicate if only one entry of this tag can exist in the * comment by specifying true as the array value, or false if more than one * is allowed. Each tag should ommit the @ symbol. Only tags other than * the standard tags should be returned. * * @return array(string => boolean) */ protected abstract function getAllowedTags(); /** * Returns the tag orders (index => tagName). * * @return array */ public function getTagOrders() { return $this->orders; }//end getTagOrders() /** * Returns the unknown tags. * * @return array */ public function getUnknown() { return $this->unknown; }//end getUnknown() }//end class ?>