1<?php 2/** 3 * PEAR_Sniffs_Functions_FunctionDeclarationSniff. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer 9 * @author Greg Sherwood <gsherwood@squiz.net> 10 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 11 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 12 * @link http://pear.php.net/package/PHP_CodeSniffer 13 */ 14 15/** 16 * PEAR_Sniffs_Functions_FunctionDeclarationSniff. 17 * 18 * Ensure single and multi-line function declarations are defined correctly. 19 * 20 * @category PHP 21 * @package PHP_CodeSniffer 22 * @author Greg Sherwood <gsherwood@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_Functions_FunctionDeclarationSniff implements PHP_CodeSniffer_Sniff 29{ 30 31 /** 32 * The number of spaces code should be indented. 33 * 34 * @var int 35 */ 36 public $indent = 4; 37 38 39 /** 40 * Returns an array of tokens this test wants to listen for. 41 * 42 * @return array 43 */ 44 public function register() 45 { 46 return array( 47 T_FUNCTION, 48 T_CLOSURE, 49 ); 50 51 }//end register() 52 53 54 /** 55 * Processes this test, when one of its tokens is encountered. 56 * 57 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 58 * @param int $stackPtr The position of the current token 59 * in the stack passed in $tokens. 60 * 61 * @return void 62 */ 63 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 64 { 65 $tokens = $phpcsFile->getTokens(); 66 67 if (isset($tokens[$stackPtr]['parenthesis_opener']) === false 68 || isset($tokens[$stackPtr]['parenthesis_closer']) === false 69 || $tokens[$stackPtr]['parenthesis_opener'] === null 70 || $tokens[$stackPtr]['parenthesis_closer'] === null 71 ) { 72 return; 73 } 74 75 $openBracket = $tokens[$stackPtr]['parenthesis_opener']; 76 $closeBracket = $tokens[$stackPtr]['parenthesis_closer']; 77 78 // Must be one space after the FUNCTION keyword. 79 if ($tokens[($stackPtr + 1)]['content'] === $phpcsFile->eolChar) { 80 $spaces = 'newline'; 81 } else if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) { 82 $spaces = strlen($tokens[($stackPtr + 1)]['content']); 83 } else { 84 $spaces = 0; 85 } 86 87 if ($spaces !== 1) { 88 $error = 'Expected 1 space after FUNCTION keyword; %s found'; 89 $data = array($spaces); 90 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterFunction', $data); 91 if ($fix === true) { 92 if ($spaces === 0) { 93 $phpcsFile->fixer->addContent($stackPtr, ' '); 94 } else { 95 $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' '); 96 } 97 } 98 } 99 100 // Must be one space before the opening parenthesis. For closures, this is 101 // enforced by the first check because there is no content between the keywords 102 // and the opening parenthesis. 103 if ($tokens[$stackPtr]['code'] === T_FUNCTION) { 104 if ($tokens[($openBracket - 1)]['content'] === $phpcsFile->eolChar) { 105 $spaces = 'newline'; 106 } else if ($tokens[($openBracket - 1)]['code'] === T_WHITESPACE) { 107 $spaces = strlen($tokens[($openBracket - 1)]['content']); 108 } else { 109 $spaces = 0; 110 } 111 112 if ($spaces !== 0) { 113 $error = 'Expected 0 spaces before opening parenthesis; %s found'; 114 $data = array($spaces); 115 $fix = $phpcsFile->addFixableError($error, $openBracket, 'SpaceBeforeOpenParen', $data); 116 if ($fix === true) { 117 $phpcsFile->fixer->replaceToken(($openBracket - 1), ''); 118 } 119 } 120 }//end if 121 122 // Must be one space before and after USE keyword for closures. 123 if ($tokens[$stackPtr]['code'] === T_CLOSURE) { 124 $use = $phpcsFile->findNext(T_USE, ($closeBracket + 1), $tokens[$stackPtr]['scope_opener']); 125 if ($use !== false) { 126 if ($tokens[($use + 1)]['code'] !== T_WHITESPACE) { 127 $length = 0; 128 } else if ($tokens[($use + 1)]['content'] === "\t") { 129 $length = '\t'; 130 } else { 131 $length = strlen($tokens[($use + 1)]['content']); 132 } 133 134 if ($length !== 1) { 135 $error = 'Expected 1 space after USE keyword; found %s'; 136 $data = array($length); 137 $fix = $phpcsFile->addFixableError($error, $use, 'SpaceAfterUse', $data); 138 if ($fix === true) { 139 if ($length === 0) { 140 $phpcsFile->fixer->addContent($use, ' '); 141 } else { 142 $phpcsFile->fixer->replaceToken(($use + 1), ' '); 143 } 144 } 145 } 146 147 if ($tokens[($use - 1)]['code'] !== T_WHITESPACE) { 148 $length = 0; 149 } else if ($tokens[($use - 1)]['content'] === "\t") { 150 $length = '\t'; 151 } else { 152 $length = strlen($tokens[($use - 1)]['content']); 153 } 154 155 if ($length !== 1) { 156 $error = 'Expected 1 space before USE keyword; found %s'; 157 $data = array($length); 158 $fix = $phpcsFile->addFixableError($error, $use, 'SpaceBeforeUse', $data); 159 if ($fix === true) { 160 if ($length === 0) { 161 $phpcsFile->fixer->addContentBefore($use, ' '); 162 } else { 163 $phpcsFile->fixer->replaceToken(($use - 1), ' '); 164 } 165 } 166 } 167 }//end if 168 }//end if 169 170 if ($this->isMultiLineDeclaration($phpcsFile, $stackPtr, $openBracket, $tokens) === true) { 171 $this->processMultiLineDeclaration($phpcsFile, $stackPtr, $tokens); 172 } else { 173 $this->processSingleLineDeclaration($phpcsFile, $stackPtr, $tokens); 174 } 175 176 }//end process() 177 178 179 /** 180 * Determine if this is a multi-line function declaration. 181 * 182 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 183 * @param int $stackPtr The position of the current token 184 * in the stack passed in $tokens. 185 * @param int $openBracket The position of the opening bracket 186 * in the stack passed in $tokens. 187 * @param array $tokens The stack of tokens that make up 188 * the file. 189 * 190 * @return void 191 */ 192 public function isMultiLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $openBracket, $tokens) 193 { 194 $closeBracket = $tokens[$openBracket]['parenthesis_closer']; 195 if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) { 196 return true; 197 } 198 199 // Closures may use the USE keyword and so be multi-line in this way. 200 if ($tokens[$stackPtr]['code'] === T_CLOSURE) { 201 $use = $phpcsFile->findNext(T_USE, ($closeBracket + 1), $tokens[$stackPtr]['scope_opener']); 202 if ($use !== false) { 203 // If the opening and closing parenthesis of the use statement 204 // are also on the same line, this is a single line declaration. 205 $open = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1)); 206 $close = $tokens[$open]['parenthesis_closer']; 207 if ($tokens[$open]['line'] !== $tokens[$close]['line']) { 208 return true; 209 } 210 } 211 } 212 213 return false; 214 215 }//end isMultiLineDeclaration() 216 217 218 /** 219 * Processes single-line declarations. 220 * 221 * Just uses the Generic BSD-Allman brace sniff. 222 * 223 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 224 * @param int $stackPtr The position of the current token 225 * in the stack passed in $tokens. 226 * @param array $tokens The stack of tokens that make up 227 * the file. 228 * 229 * @return void 230 */ 231 public function processSingleLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens) 232 { 233 if ($tokens[$stackPtr]['code'] === T_CLOSURE) { 234 if (class_exists('Generic_Sniffs_Functions_OpeningFunctionBraceKernighanRitchieSniff', true) === false) { 235 throw new PHP_CodeSniffer_Exception('Class Generic_Sniffs_Functions_OpeningFunctionBraceKernighanRitchieSniff not found'); 236 } 237 238 $sniff = new Generic_Sniffs_Functions_OpeningFunctionBraceKernighanRitchieSniff(); 239 } else { 240 if (class_exists('Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff', true) === false) { 241 throw new PHP_CodeSniffer_Exception('Class Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff not found'); 242 } 243 244 $sniff = new Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff(); 245 } 246 247 $sniff->checkClosures = true; 248 $sniff->process($phpcsFile, $stackPtr); 249 250 }//end processSingleLineDeclaration() 251 252 253 /** 254 * Processes multi-line declarations. 255 * 256 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 257 * @param int $stackPtr The position of the current token 258 * in the stack passed in $tokens. 259 * @param array $tokens The stack of tokens that make up 260 * the file. 261 * 262 * @return void 263 */ 264 public function processMultiLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens) 265 { 266 // We need to work out how far indented the function 267 // declaration itself is, so we can work out how far to 268 // indent parameters. 269 $functionIndent = 0; 270 for ($i = ($stackPtr - 1); $i >= 0; $i--) { 271 if ($tokens[$i]['line'] !== $tokens[$stackPtr]['line']) { 272 $i++; 273 break; 274 } 275 } 276 277 if ($tokens[$i]['code'] === T_WHITESPACE) { 278 $functionIndent = strlen($tokens[$i]['content']); 279 } 280 281 // The closing parenthesis must be on a new line, even 282 // when checking abstract function definitions. 283 $closeBracket = $tokens[$stackPtr]['parenthesis_closer']; 284 $prev = $phpcsFile->findPrevious( 285 T_WHITESPACE, 286 ($closeBracket - 1), 287 null, 288 true 289 ); 290 291 if ($tokens[$closeBracket]['line'] !== $tokens[$tokens[$closeBracket]['parenthesis_opener']]['line']) { 292 if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) { 293 $error = 'The closing parenthesis of a multi-line function declaration must be on a new line'; 294 $fix = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketLine'); 295 if ($fix === true) { 296 $phpcsFile->fixer->addNewlineBefore($closeBracket); 297 } 298 } 299 } 300 301 // If this is a closure and is using a USE statement, the closing 302 // parenthesis we need to look at from now on is the closing parenthesis 303 // of the USE statement. 304 if ($tokens[$stackPtr]['code'] === T_CLOSURE) { 305 $use = $phpcsFile->findNext(T_USE, ($closeBracket + 1), $tokens[$stackPtr]['scope_opener']); 306 if ($use !== false) { 307 $open = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1)); 308 $closeBracket = $tokens[$open]['parenthesis_closer']; 309 310 $prev = $phpcsFile->findPrevious( 311 T_WHITESPACE, 312 ($closeBracket - 1), 313 null, 314 true 315 ); 316 317 if ($tokens[$closeBracket]['line'] !== $tokens[$tokens[$closeBracket]['parenthesis_opener']]['line']) { 318 if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) { 319 $error = 'The closing parenthesis of a multi-line use declaration must be on a new line'; 320 $fix = $phpcsFile->addFixableError($error, $closeBracket, 'UseCloseBracketLine'); 321 if ($fix === true) { 322 $phpcsFile->fixer->addNewlineBefore($closeBracket); 323 } 324 } 325 } 326 }//end if 327 }//end if 328 329 // Each line between the parenthesis should be indented 4 spaces. 330 $openBracket = $tokens[$stackPtr]['parenthesis_opener']; 331 $lastLine = $tokens[$openBracket]['line']; 332 for ($i = ($openBracket + 1); $i < $closeBracket; $i++) { 333 if ($tokens[$i]['line'] !== $lastLine) { 334 if ($i === $tokens[$stackPtr]['parenthesis_closer'] 335 || ($tokens[$i]['code'] === T_WHITESPACE 336 && (($i + 1) === $closeBracket 337 || ($i + 1) === $tokens[$stackPtr]['parenthesis_closer'])) 338 ) { 339 // Closing braces need to be indented to the same level 340 // as the function. 341 $expectedIndent = $functionIndent; 342 } else { 343 $expectedIndent = ($functionIndent + $this->indent); 344 } 345 346 // We changed lines, so this should be a whitespace indent token. 347 if ($tokens[$i]['code'] !== T_WHITESPACE) { 348 $foundIndent = 0; 349 } else if ($tokens[$i]['line'] !== $tokens[($i + 1)]['line']) { 350 // This is an empty line, so don't check the indent. 351 $foundIndent = $expectedIndent; 352 353 $error = 'Blank lines are not allowed in a multi-line function declaration'; 354 $fix = $phpcsFile->addFixableError($error, $i, 'EmptyLine'); 355 if ($fix === true) { 356 $phpcsFile->fixer->replaceToken($i, ''); 357 } 358 } else { 359 $foundIndent = strlen($tokens[$i]['content']); 360 } 361 362 if ($expectedIndent !== $foundIndent) { 363 $error = 'Multi-line function declaration not indented correctly; expected %s spaces but found %s'; 364 $data = array( 365 $expectedIndent, 366 $foundIndent, 367 ); 368 369 $fix = $phpcsFile->addFixableError($error, $i, 'Indent', $data); 370 if ($fix === true) { 371 $spaces = str_repeat(' ', $expectedIndent); 372 if ($foundIndent === 0) { 373 $phpcsFile->fixer->addContentBefore($i, $spaces); 374 } else { 375 $phpcsFile->fixer->replaceToken($i, $spaces); 376 } 377 } 378 } 379 380 $lastLine = $tokens[$i]['line']; 381 }//end if 382 383 if ($tokens[$i]['code'] === T_ARRAY || $tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { 384 // Skip arrays as they have their own indentation rules. 385 if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { 386 $i = $tokens[$i]['bracket_closer']; 387 } else { 388 $i = $tokens[$i]['parenthesis_closer']; 389 } 390 391 $lastLine = $tokens[$i]['line']; 392 continue; 393 } 394 }//end for 395 396 if (isset($tokens[$stackPtr]['scope_opener']) === false) { 397 return; 398 } 399 400 // The opening brace needs to be one space away from the closing parenthesis. 401 $opener = $tokens[$stackPtr]['scope_opener']; 402 if ($tokens[$opener]['line'] !== $tokens[$closeBracket]['line']) { 403 $error = 'The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line'; 404 $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineBeforeOpenBrace'); 405 if ($fix === true) { 406 $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($opener - 1), $closeBracket, true); 407 $phpcsFile->fixer->beginChangeset(); 408 $phpcsFile->fixer->addContent($prev, ' {'); 409 $phpcsFile->fixer->replaceToken($opener, ''); 410 $phpcsFile->fixer->endChangeset(); 411 } 412 } else { 413 $prev = $tokens[($opener - 1)]; 414 if ($prev['code'] !== T_WHITESPACE) { 415 $length = 0; 416 } else { 417 $length = strlen($prev['content']); 418 } 419 420 if ($length !== 1) { 421 $error = 'There must be a single space between the closing parenthesis and the opening brace of a multi-line function declaration; found %s spaces'; 422 $fix = $phpcsFile->addFixableError($error, ($opener - 1), 'SpaceBeforeOpenBrace', array($length)); 423 if ($fix === true) { 424 if ($length === 0) { 425 $phpcsFile->fixer->addContentBefore($opener, ' '); 426 } else { 427 $phpcsFile->fixer->replaceToken(($opener - 1), ' '); 428 } 429 } 430 431 return; 432 }//end if 433 }//end if 434 435 }//end processMultiLineDeclaration() 436 437 438}//end class 439