1<?php 2/** 3 * Parses and verifies the doc comments for functions. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer 9 * @author Greg Sherwood <gsherwood@squiz.net> 10 * @author Marc McIntyre <mmcintyre@squiz.net> 11 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 12 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 13 * @link http://pear.php.net/package/PHP_CodeSniffer 14 */ 15 16/** 17 * Parses and verifies the doc comments for functions. 18 * 19 * @category PHP 20 * @package PHP_CodeSniffer 21 * @author Greg Sherwood <gsherwood@squiz.net> 22 * @author Marc McIntyre <mmcintyre@squiz.net> 23 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 24 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 25 * @version Release: @package_version@ 26 * @link http://pear.php.net/package/PHP_CodeSniffer 27 */ 28class PEAR_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_Sniff 29{ 30 31 32 /** 33 * Returns an array of tokens this test wants to listen for. 34 * 35 * @return array 36 */ 37 public function register() 38 { 39 return array(T_FUNCTION); 40 41 }//end register() 42 43 44 /** 45 * Processes this test, when one of its tokens is encountered. 46 * 47 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 48 * @param int $stackPtr The position of the current token 49 * in the stack passed in $tokens. 50 * 51 * @return void 52 */ 53 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 54 { 55 $tokens = $phpcsFile->getTokens(); 56 $find = PHP_CodeSniffer_Tokens::$methodPrefixes; 57 $find[] = T_WHITESPACE; 58 59 $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true); 60 if ($tokens[$commentEnd]['code'] === T_COMMENT) { 61 // Inline comments might just be closing comments for 62 // control structures or functions instead of function comments 63 // using the wrong comment type. If there is other code on the line, 64 // assume they relate to that code. 65 $prev = $phpcsFile->findPrevious($find, ($commentEnd - 1), null, true); 66 if ($prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line']) { 67 $commentEnd = $prev; 68 } 69 } 70 71 if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG 72 && $tokens[$commentEnd]['code'] !== T_COMMENT 73 ) { 74 $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing'); 75 $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'no'); 76 return; 77 } else { 78 $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'yes'); 79 } 80 81 if ($tokens[$commentEnd]['code'] === T_COMMENT) { 82 $phpcsFile->addError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle'); 83 return; 84 } 85 86 if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) { 87 $error = 'There must be no blank lines after the function comment'; 88 $phpcsFile->addError($error, $commentEnd, 'SpacingAfter'); 89 } 90 91 $commentStart = $tokens[$commentEnd]['comment_opener']; 92 foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 93 if ($tokens[$tag]['content'] === '@see') { 94 // Make sure the tag isn't empty. 95 $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); 96 if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { 97 $error = 'Content missing for @see tag in function comment'; 98 $phpcsFile->addError($error, $tag, 'EmptySees'); 99 } 100 } 101 } 102 103 $this->processReturn($phpcsFile, $stackPtr, $commentStart); 104 $this->processThrows($phpcsFile, $stackPtr, $commentStart); 105 $this->processParams($phpcsFile, $stackPtr, $commentStart); 106 107 }//end process() 108 109 110 /** 111 * Process the return comment of this function comment. 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 * @param int $commentStart The position in the stack where the comment started. 117 * 118 * @return void 119 */ 120 protected function processReturn(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 121 { 122 $tokens = $phpcsFile->getTokens(); 123 124 // Skip constructor and destructor. 125 $methodName = $phpcsFile->getDeclarationName($stackPtr); 126 $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); 127 128 $return = null; 129 foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 130 if ($tokens[$tag]['content'] === '@return') { 131 if ($return !== null) { 132 $error = 'Only 1 @return tag is allowed in a function comment'; 133 $phpcsFile->addError($error, $tag, 'DuplicateReturn'); 134 return; 135 } 136 137 $return = $tag; 138 } 139 } 140 141 if ($isSpecialMethod === true) { 142 return; 143 } 144 145 if ($return !== null) { 146 $content = $tokens[($return + 2)]['content']; 147 if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { 148 $error = 'Return type missing for @return tag in function comment'; 149 $phpcsFile->addError($error, $return, 'MissingReturnType'); 150 } 151 } else { 152 $error = 'Missing @return tag in function comment'; 153 $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn'); 154 }//end if 155 156 }//end processReturn() 157 158 159 /** 160 * Process any throw tags that this function comment has. 161 * 162 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 163 * @param int $stackPtr The position of the current token 164 * in the stack passed in $tokens. 165 * @param int $commentStart The position in the stack where the comment started. 166 * 167 * @return void 168 */ 169 protected function processThrows(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 170 { 171 $tokens = $phpcsFile->getTokens(); 172 173 $throws = array(); 174 foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 175 if ($tokens[$tag]['content'] !== '@throws') { 176 continue; 177 } 178 179 $exception = null; 180 $comment = null; 181 if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { 182 $matches = array(); 183 preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches); 184 $exception = $matches[1]; 185 if (isset($matches[2]) === true) { 186 $comment = $matches[2]; 187 } 188 } 189 190 if ($exception === null) { 191 $error = 'Exception type missing for @throws tag in function comment'; 192 $phpcsFile->addError($error, $tag, 'InvalidThrows'); 193 } 194 }//end foreach 195 196 }//end processThrows() 197 198 199 /** 200 * Process the function parameter comments. 201 * 202 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 203 * @param int $stackPtr The position of the current token 204 * in the stack passed in $tokens. 205 * @param int $commentStart The position in the stack where the comment started. 206 * 207 * @return void 208 */ 209 protected function processParams(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 210 { 211 $tokens = $phpcsFile->getTokens(); 212 213 $params = array(); 214 $maxType = 0; 215 $maxVar = 0; 216 foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { 217 if ($tokens[$tag]['content'] !== '@param') { 218 continue; 219 } 220 221 $type = ''; 222 $typeSpace = 0; 223 $var = ''; 224 $varSpace = 0; 225 $comment = ''; 226 $commentEnd = 0; 227 if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { 228 $matches = array(); 229 preg_match('/([^$&.]+)(?:((?:\.\.\.)?(?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches); 230 231 if (empty($matches) === false) { 232 $typeLen = strlen($matches[1]); 233 $type = trim($matches[1]); 234 $typeSpace = ($typeLen - strlen($type)); 235 $typeLen = strlen($type); 236 if ($typeLen > $maxType) { 237 $maxType = $typeLen; 238 } 239 } 240 241 if (isset($matches[2]) === true) { 242 $var = $matches[2]; 243 $varLen = strlen($var); 244 if ($varLen > $maxVar) { 245 $maxVar = $varLen; 246 } 247 248 if (isset($matches[4]) === true) { 249 $varSpace = strlen($matches[3]); 250 $comment = $matches[4]; 251 252 // Any strings until the next tag belong to this comment. 253 if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { 254 $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; 255 } else { 256 $end = $tokens[$commentStart]['comment_closer']; 257 } 258 259 for ($i = ($tag + 3); $i < $end; $i++) { 260 if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { 261 $comment .= ' '.$tokens[$i]['content']; 262 $commentEnd = $i; 263 } 264 } 265 } else { 266 $error = 'Missing parameter comment'; 267 $phpcsFile->addError($error, $tag, 'MissingParamComment'); 268 }//end if 269 } else { 270 $error = 'Missing parameter name'; 271 $phpcsFile->addError($error, $tag, 'MissingParamName'); 272 }//end if 273 } else { 274 $error = 'Missing parameter type'; 275 $phpcsFile->addError($error, $tag, 'MissingParamType'); 276 }//end if 277 278 $params[] = array( 279 'tag' => $tag, 280 'type' => $type, 281 'var' => $var, 282 'comment' => $comment, 283 'comment_end' => $commentEnd, 284 'type_space' => $typeSpace, 285 'var_space' => $varSpace, 286 ); 287 }//end foreach 288 289 $realParams = $phpcsFile->getMethodParameters($stackPtr); 290 $foundParams = array(); 291 292 // We want to use ... for all variable length arguments, so added 293 // this prefix to the variable name so comparisons are easier. 294 foreach ($realParams as $pos => $param) { 295 if ($param['variable_length'] === true) { 296 $realParams[$pos]['name'] = '...'.$realParams[$pos]['name']; 297 } 298 } 299 300 foreach ($params as $pos => $param) { 301 if ($param['var'] === '') { 302 continue; 303 } 304 305 $foundParams[] = $param['var']; 306 307 // Check number of spaces after the type. 308 $spaces = ($maxType - strlen($param['type']) + 1); 309 if ($param['type_space'] !== $spaces) { 310 $error = 'Expected %s spaces after parameter type; %s found'; 311 $data = array( 312 $spaces, 313 $param['type_space'], 314 ); 315 316 $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data); 317 if ($fix === true) { 318 $commentToken = ($param['tag'] + 2); 319 320 $content = $param['type']; 321 $content .= str_repeat(' ', $spaces); 322 $content .= $param['var']; 323 $content .= str_repeat(' ', $param['var_space']); 324 325 $wrapLength = ($tokens[$commentToken]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']) 326 ); 327 328 $star = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']); 329 $spaceLength = (strlen($content) + $tokens[($commentToken - 1)]['length'] + $tokens[($commentToken - 2)]['length'] 330 ); 331 332 $padding = str_repeat(' ', ($tokens[$star]['column'] - 1)); 333 $padding .= '* '; 334 $padding .= str_repeat(' ', $spaceLength); 335 336 $content .= wordwrap( 337 $param['comment'], 338 $wrapLength, 339 $phpcsFile->eolChar.$padding 340 ); 341 342 $phpcsFile->fixer->replaceToken($commentToken, $content); 343 for ($i = ($commentToken + 1); $i <= $param['comment_end']; $i++) { 344 $phpcsFile->fixer->replaceToken($i, ''); 345 } 346 }//end if 347 }//end if 348 349 // Make sure the param name is correct. 350 if (isset($realParams[$pos]) === true) { 351 $realName = $realParams[$pos]['name']; 352 if ($realName !== $param['var']) { 353 $code = 'ParamNameNoMatch'; 354 $data = array( 355 $param['var'], 356 $realName, 357 ); 358 359 $error = 'Doc comment for parameter %s does not match '; 360 if (strtolower($param['var']) === strtolower($realName)) { 361 $error .= 'case of '; 362 $code = 'ParamNameNoCaseMatch'; 363 } 364 365 $error .= 'actual variable name %s'; 366 367 $phpcsFile->addError($error, $param['tag'], $code, $data); 368 } 369 } else if (substr($param['var'], -4) !== ',...') { 370 // We must have an extra parameter comment. 371 $error = 'Superfluous parameter comment'; 372 $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment'); 373 }//end if 374 375 if ($param['comment'] === '') { 376 continue; 377 } 378 379 // Check number of spaces after the var name. 380 $spaces = ($maxVar - strlen($param['var']) + 1); 381 if ($param['var_space'] !== $spaces) { 382 $error = 'Expected %s spaces after parameter name; %s found'; 383 $data = array( 384 $spaces, 385 $param['var_space'], 386 ); 387 388 $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data); 389 if ($fix === true) { 390 $commentToken = ($param['tag'] + 2); 391 392 $content = $param['type']; 393 $content .= str_repeat(' ', $param['type_space']); 394 $content .= $param['var']; 395 $content .= str_repeat(' ', $spaces); 396 397 $wrapLength = ($tokens[$commentToken]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']) 398 ); 399 400 $star = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']); 401 $spaceLength = (strlen($content) + $tokens[($commentToken - 1)]['length'] + $tokens[($commentToken - 2)]['length'] 402 ); 403 404 $padding = str_repeat(' ', ($tokens[$star]['column'] - 1)); 405 $padding .= '* '; 406 $padding .= str_repeat(' ', $spaceLength); 407 408 $content .= wordwrap( 409 $param['comment'], 410 $wrapLength, 411 $phpcsFile->eolChar.$padding 412 ); 413 414 $phpcsFile->fixer->replaceToken($commentToken, $content); 415 for ($i = ($commentToken + 1); $i <= $param['comment_end']; $i++) { 416 $phpcsFile->fixer->replaceToken($i, ''); 417 } 418 }//end if 419 }//end if 420 }//end foreach 421 422 $realNames = array(); 423 foreach ($realParams as $realParam) { 424 $realNames[] = $realParam['name']; 425 } 426 427 // Report missing comments. 428 $diff = array_diff($realNames, $foundParams); 429 foreach ($diff as $neededParam) { 430 $error = 'Doc comment for parameter "%s" missing'; 431 $data = array($neededParam); 432 $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data); 433 } 434 435 }//end processParams() 436 437 438}//end class 439