1<?php 2/** 3 * Verifies that a @throws tag exists for a function that throws exceptions. 4 * Verifies the number of @throws tags and the number of throw tokens matches. 5 * Verifies the exception type. 6 * 7 * PHP version 5 8 * 9 * @category PHP 10 * @package PHP_CodeSniffer 11 * @author Greg Sherwood <gsherwood@squiz.net> 12 * @author Marc McIntyre <mmcintyre@squiz.net> 13 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 14 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 15 * @link http://pear.php.net/package/PHP_CodeSniffer 16 */ 17 18if (class_exists('PHP_CodeSniffer_Standards_AbstractScopeSniff', true) === false) { 19 $error = 'Class PHP_CodeSniffer_Standards_AbstractScopeSniff not found'; 20 throw new PHP_CodeSniffer_Exception($error); 21} 22 23/** 24 * Verifies that a @throws tag exists for a function that throws exceptions. 25 * Verifies the number of @throws tags and the number of throw tokens matches. 26 * Verifies the exception type. 27 * 28 * @category PHP 29 * @package PHP_CodeSniffer 30 * @author Greg Sherwood <gsherwood@squiz.net> 31 * @author Marc McIntyre <mmcintyre@squiz.net> 32 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 33 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 34 * @version Release: @package_version@ 35 * @link http://pear.php.net/package/PHP_CodeSniffer 36 */ 37class Squiz_Sniffs_Commenting_FunctionCommentThrowTagSniff extends PHP_CodeSniffer_Standards_AbstractScopeSniff 38{ 39 40 41 /** 42 * Constructs a Squiz_Sniffs_Commenting_FunctionCommentThrowTagSniff. 43 */ 44 public function __construct() 45 { 46 parent::__construct(array(T_FUNCTION), array(T_THROW)); 47 48 }//end __construct() 49 50 51 /** 52 * Processes the function tokens within the class. 53 * 54 * @param PHP_CodeSniffer_File $phpcsFile The file where this token was found. 55 * @param int $stackPtr The position where the token was found. 56 * @param int $currScope The current scope opener token. 57 * 58 * @return void 59 */ 60 protected function processTokenWithinScope(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $currScope) 61 { 62 // Is this the first throw token within the current function scope? 63 // If so, we have to validate other throw tokens within the same scope. 64 $previousThrow = $phpcsFile->findPrevious(T_THROW, ($stackPtr - 1), $currScope); 65 if ($previousThrow !== false) { 66 return; 67 } 68 69 $tokens = $phpcsFile->getTokens(); 70 71 $find = PHP_CodeSniffer_Tokens::$methodPrefixes; 72 $find[] = T_WHITESPACE; 73 74 $commentEnd = $phpcsFile->findPrevious($find, ($currScope - 1), null, true); 75 if ($tokens[$commentEnd]['code'] === T_COMMENT) { 76 // Function is using the wrong type of comment. 77 return; 78 } 79 80 if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG 81 && $tokens[$commentEnd]['code'] !== T_COMMENT 82 ) { 83 // Function doesn't have a doc comment. 84 return; 85 } 86 87 $currScopeEnd = $tokens[$currScope]['scope_closer']; 88 89 // Find all the exception type token within the current scope. 90 $throwTokens = array(); 91 $currPos = $stackPtr; 92 $foundThrows = false; 93 while ($currPos < $currScopeEnd && $currPos !== false) { 94 if ($phpcsFile->hasCondition($currPos, T_CLOSURE) === false) { 95 $foundThrows = true; 96 97 /* 98 If we can't find a NEW, we are probably throwing 99 a variable. 100 101 If we're throwing the same variable as the exception container 102 from the nearest 'catch' block, we take that exception, as it is 103 likely to be a re-throw. 104 105 If we can't find a matching catch block, or the variable name 106 is different, it's probably a different variable, so we ignore it, 107 but they still need to provide at least one @throws tag, even through we 108 don't know the exception class. 109 */ 110 111 $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($currPos + 1), null, true); 112 if ($tokens[$nextToken]['code'] === T_NEW) { 113 $currException = $phpcsFile->findNext( 114 array( 115 T_NS_SEPARATOR, 116 T_STRING, 117 ), 118 $currPos, 119 $currScopeEnd, 120 false, 121 null, 122 true 123 ); 124 125 if ($currException !== false) { 126 $endException = $phpcsFile->findNext( 127 array( 128 T_NS_SEPARATOR, 129 T_STRING, 130 ), 131 ($currException + 1), 132 $currScopeEnd, 133 true, 134 null, 135 true 136 ); 137 138 if ($endException === false) { 139 $throwTokens[] = $tokens[$currException]['content']; 140 } else { 141 $throwTokens[] = $phpcsFile->getTokensAsString($currException, ($endException - $currException)); 142 } 143 }//end if 144 } else if ($tokens[$nextToken]['code'] === T_VARIABLE) { 145 // Find where the nearest 'catch' block in this scope. 146 $catch = $phpcsFile->findPrevious( 147 T_CATCH, 148 $currPos, 149 $tokens[$currScope]['scope_opener'], 150 false, 151 null, 152 false 153 ); 154 155 if ($catch !== false) { 156 // Get the start of the 'catch' exception. 157 $currException = $phpcsFile->findNext( 158 array( 159 T_NS_SEPARATOR, 160 T_STRING, 161 ), 162 $tokens[$catch]['parenthesis_opener'], 163 $tokens[$catch]['parenthesis_closer'], 164 false, 165 null, 166 true 167 ); 168 169 if ($currException !== false) { 170 // Find the next whitespace (which should be the end of the exception). 171 $endException = $phpcsFile->findNext( 172 T_WHITESPACE, 173 ($currException + 1), 174 $tokens[$catch]['parenthesis_closer'], 175 false, 176 null, 177 true 178 ); 179 180 if ($endException !== false) { 181 // Find the variable that we're catching into. 182 $thrownVar = $phpcsFile->findNext( 183 T_VARIABLE, 184 ($endException + 1), 185 $tokens[$catch]["parenthesis_closer"], 186 false, 187 null, 188 true 189 ); 190 191 // Sanity check that the variable that the exception is caught into is the one that's thrown. 192 if ($tokens[$thrownVar]['content'] === $tokens[$nextToken]['content']) { 193 $throwTokens[] = $phpcsFile->getTokensAsString($currException, ($endException - $currException)); 194 }//end if 195 }//end if 196 }//end if 197 }//end if 198 }//end if 199 }//end if 200 201 $currPos = $phpcsFile->findNext(T_THROW, ($currPos + 1), $currScopeEnd); 202 }//end while 203 204 if ($foundThrows === false) { 205 return; 206 } 207 208 // Only need one @throws tag for each type of exception thrown. 209 $throwTokens = array_unique($throwTokens); 210 211 $throwTags = array(); 212 $commentStart = $tokens[$commentEnd]['comment_opener']; 213 foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 214 if ($tokens[$tag]['content'] !== '@throws') { 215 continue; 216 } 217 218 if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { 219 $exception = $tokens[($tag + 2)]['content']; 220 $space = strpos($exception, ' '); 221 if ($space !== false) { 222 $exception = substr($exception, 0, $space); 223 } 224 225 $throwTags[$exception] = true; 226 } 227 } 228 229 if (empty($throwTags) === true) { 230 $error = 'Missing @throws tag in function comment'; 231 $phpcsFile->addError($error, $commentEnd, 'Missing'); 232 return; 233 } else if (empty($throwTokens) === true) { 234 // If token count is zero, it means that only variables are being 235 // thrown, so we need at least one @throws tag (checked above). 236 // Nothing more to do. 237 return; 238 } 239 240 // Make sure @throws tag count matches throw token count. 241 $tokenCount = count($throwTokens); 242 $tagCount = count($throwTags); 243 if ($tokenCount !== $tagCount) { 244 $error = 'Expected %s @throws tag(s) in function comment; %s found'; 245 $data = array( 246 $tokenCount, 247 $tagCount, 248 ); 249 $phpcsFile->addError($error, $commentEnd, 'WrongNumber', $data); 250 return; 251 } 252 253 foreach ($throwTokens as $throw) { 254 if (isset($throwTags[$throw]) === false) { 255 $error = 'Missing @throws tag for "%s" exception'; 256 $data = array($throw); 257 $phpcsFile->addError($error, $commentEnd, 'Missing', $data); 258 } 259 } 260 261 }//end processTokenWithinScope() 262 263 264}//end class 265