1<?php 2/** 3 * PEAR_Sniffs_Functions_FunctionCallSignatureSniff. 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 * PEAR_Sniffs_Functions_FunctionCallSignatureSniff. 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_Functions_FunctionCallSignatureSniff implements PHP_CodeSniffer_Sniff 29{ 30 31 /** 32 * A list of tokenizers this sniff supports. 33 * 34 * @var array 35 */ 36 public $supportedTokenizers = array( 37 'PHP', 38 'JS', 39 ); 40 41 /** 42 * The number of spaces code should be indented. 43 * 44 * @var int 45 */ 46 public $indent = 4; 47 48 /** 49 * If TRUE, multiple arguments can be defined per line in a multi-line call. 50 * 51 * @var bool 52 */ 53 public $allowMultipleArguments = true; 54 55 /** 56 * How many spaces should follow the opening bracket. 57 * 58 * @var int 59 */ 60 public $requiredSpacesAfterOpen = 0; 61 62 /** 63 * How many spaces should precede the closing bracket. 64 * 65 * @var int 66 */ 67 public $requiredSpacesBeforeClose = 0; 68 69 70 /** 71 * Returns an array of tokens this test wants to listen for. 72 * 73 * @return array 74 */ 75 public function register() 76 { 77 return PHP_CodeSniffer_Tokens::$functionNameTokens; 78 79 }//end register() 80 81 82 /** 83 * Processes this test, when one of its tokens is encountered. 84 * 85 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 86 * @param int $stackPtr The position of the current token 87 * in the stack passed in $tokens. 88 * 89 * @return void 90 */ 91 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 92 { 93 $this->requiredSpacesAfterOpen = (int) $this->requiredSpacesAfterOpen; 94 $this->requiredSpacesBeforeClose = (int) $this->requiredSpacesBeforeClose; 95 $tokens = $phpcsFile->getTokens(); 96 97 // Find the next non-empty token. 98 $openBracket = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr + 1), null, true); 99 100 if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) { 101 // Not a function call. 102 return; 103 } 104 105 if (isset($tokens[$openBracket]['parenthesis_closer']) === false) { 106 // Not a function call. 107 return; 108 } 109 110 // Find the previous non-empty token. 111 $search = PHP_CodeSniffer_Tokens::$emptyTokens; 112 $search[] = T_BITWISE_AND; 113 $previous = $phpcsFile->findPrevious($search, ($stackPtr - 1), null, true); 114 if ($tokens[$previous]['code'] === T_FUNCTION) { 115 // It's a function definition, not a function call. 116 return; 117 } 118 119 $closeBracket = $tokens[$openBracket]['parenthesis_closer']; 120 121 if (($stackPtr + 1) !== $openBracket) { 122 // Checking this: $value = my_function[*](...). 123 $error = 'Space before opening parenthesis of function call prohibited'; 124 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeOpenBracket'); 125 if ($fix === true) { 126 $phpcsFile->fixer->beginChangeset(); 127 for ($i = ($stackPtr + 1); $i < $openBracket; $i++) { 128 $phpcsFile->fixer->replaceToken($i, ''); 129 } 130 131 // Modify the bracket as well to ensure a conflict if the bracket 132 // has been changed in some way by another sniff. 133 $phpcsFile->fixer->replaceToken($openBracket, '('); 134 $phpcsFile->fixer->endChangeset(); 135 } 136 } 137 138 $next = $phpcsFile->findNext(T_WHITESPACE, ($closeBracket + 1), null, true); 139 if ($tokens[$next]['code'] === T_SEMICOLON) { 140 if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[($closeBracket + 1)]['code']]) === true) { 141 $error = 'Space after closing parenthesis of function call prohibited'; 142 $fix = $phpcsFile->addFixableError($error, $closeBracket, 'SpaceAfterCloseBracket'); 143 if ($fix === true) { 144 $phpcsFile->fixer->beginChangeset(); 145 for ($i = ($closeBracket + 1); $i < $next; $i++) { 146 $phpcsFile->fixer->replaceToken($i, ''); 147 } 148 149 // Modify the bracket as well to ensure a conflict if the bracket 150 // has been changed in some way by another sniff. 151 $phpcsFile->fixer->replaceToken($closeBracket, ')'); 152 $phpcsFile->fixer->endChangeset(); 153 } 154 } 155 } 156 157 // Check if this is a single line or multi-line function call. 158 if ($this->isMultiLineCall($phpcsFile, $stackPtr, $openBracket, $tokens) === true) { 159 $this->processMultiLineCall($phpcsFile, $stackPtr, $openBracket, $tokens); 160 } else { 161 $this->processSingleLineCall($phpcsFile, $stackPtr, $openBracket, $tokens); 162 } 163 164 }//end process() 165 166 167 /** 168 * Determine if this is a multi-line function call. 169 * 170 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 171 * @param int $stackPtr The position of the current token 172 * in the stack passed in $tokens. 173 * @param int $openBracket The position of the opening bracket 174 * in the stack passed in $tokens. 175 * @param array $tokens The stack of tokens that make up 176 * the file. 177 * 178 * @return void 179 */ 180 public function isMultiLineCall(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $openBracket, $tokens) 181 { 182 $closeBracket = $tokens[$openBracket]['parenthesis_closer']; 183 if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) { 184 return true; 185 } 186 187 return false; 188 189 }//end isMultiLineCall() 190 191 192 /** 193 * Processes single-line calls. 194 * 195 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 196 * @param int $stackPtr The position of the current token 197 * in the stack passed in $tokens. 198 * @param int $openBracket The position of the opening bracket 199 * in the stack passed in $tokens. 200 * @param array $tokens The stack of tokens that make up 201 * the file. 202 * 203 * @return void 204 */ 205 public function processSingleLineCall(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $openBracket, $tokens) 206 { 207 $closer = $tokens[$openBracket]['parenthesis_closer']; 208 if ($openBracket === ($closer - 1)) { 209 return; 210 } 211 212 if ($this->requiredSpacesAfterOpen === 0 && $tokens[($openBracket + 1)]['code'] === T_WHITESPACE) { 213 // Checking this: $value = my_function([*]...). 214 $error = 'Space after opening parenthesis of function call prohibited'; 215 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterOpenBracket'); 216 if ($fix === true) { 217 $phpcsFile->fixer->replaceToken(($openBracket + 1), ''); 218 } 219 } else if ($this->requiredSpacesAfterOpen > 0) { 220 $spaceAfterOpen = 0; 221 if ($tokens[($openBracket + 1)]['code'] === T_WHITESPACE) { 222 $spaceAfterOpen = strlen($tokens[($openBracket + 1)]['content']); 223 } 224 225 if ($spaceAfterOpen !== $this->requiredSpacesAfterOpen) { 226 $error = 'Expected %s spaces after opening bracket; %s found'; 227 $data = array( 228 $this->requiredSpacesAfterOpen, 229 $spaceAfterOpen, 230 ); 231 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterOpenBracket', $data); 232 if ($fix === true) { 233 $padding = str_repeat(' ', $this->requiredSpacesAfterOpen); 234 if ($spaceAfterOpen === 0) { 235 $phpcsFile->fixer->addContent($openBracket, $padding); 236 } else { 237 $phpcsFile->fixer->replaceToken(($openBracket + 1), $padding); 238 } 239 } 240 } 241 }//end if 242 243 // Checking this: $value = my_function(...[*]). 244 $spaceBeforeClose = 0; 245 $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($closer - 1), $openBracket, true); 246 if ($tokens[$prev]['code'] === T_END_HEREDOC || $tokens[$prev]['code'] === T_END_NOWDOC) { 247 // Need a newline after these tokens, so ignore this rule. 248 return; 249 } 250 251 if ($tokens[$prev]['line'] !== $tokens[$closer]['line']) { 252 $spaceBeforeClose = 'newline'; 253 } else if ($tokens[($closer - 1)]['code'] === T_WHITESPACE) { 254 $spaceBeforeClose = strlen($tokens[($closer - 1)]['content']); 255 } 256 257 if ($spaceBeforeClose !== $this->requiredSpacesBeforeClose) { 258 $error = 'Expected %s spaces before closing bracket; %s found'; 259 $data = array( 260 $this->requiredSpacesBeforeClose, 261 $spaceBeforeClose, 262 ); 263 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeCloseBracket', $data); 264 if ($fix === true) { 265 $padding = str_repeat(' ', $this->requiredSpacesBeforeClose); 266 267 if ($spaceBeforeClose === 0) { 268 $phpcsFile->fixer->addContentBefore($closer, $padding); 269 } else if ($spaceBeforeClose === 'newline') { 270 $phpcsFile->fixer->beginChangeset(); 271 272 $closingContent = ')'; 273 274 $next = $phpcsFile->findNext(T_WHITESPACE, ($closer + 1), null, true); 275 if ($tokens[$next]['code'] === T_SEMICOLON) { 276 $closingContent .= ';'; 277 for ($i = ($closer + 1); $i <= $next; $i++) { 278 $phpcsFile->fixer->replaceToken($i, ''); 279 } 280 } 281 282 // We want to jump over any whitespace or inline comment and 283 // move the closing parenthesis after any other token. 284 $prev = ($closer - 1); 285 while (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$prev]['code']]) === true) { 286 if (($tokens[$prev]['code'] === T_COMMENT) 287 && (strpos($tokens[$prev]['content'], '*/') !== false) 288 ) { 289 break; 290 } 291 292 $prev--; 293 } 294 295 $phpcsFile->fixer->addContent($prev, $padding.$closingContent); 296 297 $prevNonWhitespace = $phpcsFile->findPrevious(T_WHITESPACE, ($closer - 1), null, true); 298 for ($i = ($prevNonWhitespace + 1); $i <= $closer; $i++) { 299 $phpcsFile->fixer->replaceToken($i, ''); 300 } 301 302 $phpcsFile->fixer->endChangeset(); 303 } else { 304 $phpcsFile->fixer->replaceToken(($closer - 1), $padding); 305 }//end if 306 }//end if 307 }//end if 308 309 }//end processSingleLineCall() 310 311 312 /** 313 * Processes multi-line calls. 314 * 315 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 316 * @param int $stackPtr The position of the current token 317 * in the stack passed in $tokens. 318 * @param int $openBracket The position of the opening bracket 319 * in the stack passed in $tokens. 320 * @param array $tokens The stack of tokens that make up 321 * the file. 322 * 323 * @return void 324 */ 325 public function processMultiLineCall(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $openBracket, $tokens) 326 { 327 // We need to work out how far indented the function 328 // call itself is, so we can work out how far to 329 // indent the arguments. 330 $start = $phpcsFile->findStartOfStatement($stackPtr); 331 foreach (array('stackPtr', 'start') as $checkToken) { 332 $x = $$checkToken; 333 for ($i = ($x - 1); $i >= 0; $i--) { 334 if ($tokens[$i]['line'] !== $tokens[$x]['line']) { 335 $i++; 336 break; 337 } 338 } 339 340 if ($i <= 0) { 341 $functionIndent = 0; 342 } else if ($tokens[$i]['code'] === T_WHITESPACE) { 343 $functionIndent = strlen($tokens[$i]['content']); 344 } else if ($tokens[$i]['code'] === T_CONSTANT_ENCAPSED_STRING) { 345 $functionIndent = 0; 346 } else { 347 $trimmed = ltrim($tokens[$i]['content']); 348 if ($trimmed === '') { 349 if ($tokens[$i]['code'] === T_INLINE_HTML) { 350 $functionIndent = strlen($tokens[$i]['content']); 351 } else { 352 $functionIndent = ($tokens[$i]['column'] - 1); 353 } 354 } else { 355 $functionIndent = (strlen($tokens[$i]['content']) - strlen($trimmed)); 356 } 357 } 358 359 $varName = $checkToken.'Indent'; 360 $$varName = $functionIndent; 361 }//end foreach 362 363 $functionIndent = max($startIndent, $stackPtrIndent); 364 365 $next = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($openBracket + 1), null, true); 366 if ($tokens[$next]['line'] === $tokens[$openBracket]['line']) { 367 $error = 'Opening parenthesis of a multi-line function call must be the last content on the line'; 368 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ContentAfterOpenBracket'); 369 if ($fix === true) { 370 $phpcsFile->fixer->addContent( 371 $openBracket, 372 $phpcsFile->eolChar.str_repeat(' ', ($functionIndent + $this->indent)) 373 ); 374 } 375 } 376 377 $closeBracket = $tokens[$openBracket]['parenthesis_closer']; 378 $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($closeBracket - 1), null, true); 379 if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) { 380 $error = 'Closing parenthesis of a multi-line function call must be on a line by itself'; 381 $fix = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketLine'); 382 if ($fix === true) { 383 $phpcsFile->fixer->addContentBefore( 384 $closeBracket, 385 $phpcsFile->eolChar.str_repeat(' ', ($functionIndent + $this->indent)) 386 ); 387 } 388 } 389 390 // Each line between the parenthesis should be indented n spaces. 391 $lastLine = ($tokens[$openBracket]['line'] - 1); 392 $argStart = null; 393 $argEnd = null; 394 $inArg = false; 395 396 // Start processing at the first argument. 397 $i = $phpcsFile->findNext(T_WHITESPACE, ($openBracket + 1), null, true); 398 if ($tokens[($i - 1)]['code'] === T_WHITESPACE 399 && $tokens[($i - 1)]['line'] === $tokens[$i]['line'] 400 ) { 401 // Make sure we check the indent. 402 $i--; 403 } 404 405 for ($i; $i < $closeBracket; $i++) { 406 if ($i > $argStart && $i < $argEnd) { 407 $inArg = true; 408 } else { 409 $inArg = false; 410 } 411 412 if ($tokens[$i]['line'] !== $lastLine) { 413 $lastLine = $tokens[$i]['line']; 414 415 // Ignore heredoc indentation. 416 if (isset(PHP_CodeSniffer_Tokens::$heredocTokens[$tokens[$i]['code']]) === true) { 417 continue; 418 } 419 420 // Ignore multi-line string indentation. 421 if (isset(PHP_CodeSniffer_Tokens::$stringTokens[$tokens[$i]['code']]) === true 422 && $tokens[$i]['code'] === $tokens[($i - 1)]['code'] 423 ) { 424 continue; 425 } 426 427 // Ignore inline HTML. 428 if ($tokens[$i]['code'] === T_INLINE_HTML) { 429 continue; 430 } 431 432 if ($tokens[$i]['line'] !== $tokens[$openBracket]['line']) { 433 // We changed lines, so this should be a whitespace indent token, but first make 434 // sure it isn't a blank line because we don't need to check indent unless there 435 // is actually some code to indent. 436 if ($tokens[$i]['code'] === T_WHITESPACE) { 437 $nextCode = $phpcsFile->findNext(T_WHITESPACE, ($i + 1), ($closeBracket + 1), true); 438 if ($tokens[$nextCode]['line'] !== $lastLine) { 439 if ($inArg === false) { 440 $error = 'Empty lines are not allowed in multi-line function calls'; 441 $fix = $phpcsFile->addFixableError($error, $i, 'EmptyLine'); 442 if ($fix === true) { 443 $phpcsFile->fixer->replaceToken($i, ''); 444 } 445 } 446 447 continue; 448 } 449 } else { 450 $nextCode = $i; 451 } 452 453 if ($tokens[$nextCode]['line'] === $tokens[$closeBracket]['line']) { 454 // Closing brace needs to be indented to the same level 455 // as the function call. 456 $inArg = false; 457 $expectedIndent = $functionIndent; 458 } else { 459 $expectedIndent = ($functionIndent + $this->indent); 460 } 461 462 if ($tokens[$i]['code'] !== T_WHITESPACE 463 && $tokens[$i]['code'] !== T_DOC_COMMENT_WHITESPACE 464 ) { 465 // Just check if it is a multi-line block comment. If so, we can 466 // calculate the indent from the whitespace before the content. 467 if ($tokens[$i]['code'] === T_COMMENT 468 && $tokens[($i - 1)]['code'] === T_COMMENT 469 ) { 470 $trimmedLength = strlen(ltrim($tokens[$i]['content'])); 471 if ($trimmedLength === 0) { 472 // This is a blank comment line, so indenting it is 473 // pointless. 474 continue; 475 } 476 477 $foundIndent = (strlen($tokens[$i]['content']) - $trimmedLength); 478 } else { 479 $foundIndent = 0; 480 } 481 } else { 482 $foundIndent = strlen($tokens[$i]['content']); 483 } 484 485 if ($foundIndent < $expectedIndent 486 || ($inArg === false 487 && $expectedIndent !== $foundIndent) 488 ) { 489 $error = 'Multi-line function call not indented correctly; expected %s spaces but found %s'; 490 $data = array( 491 $expectedIndent, 492 $foundIndent, 493 ); 494 495 $fix = $phpcsFile->addFixableError($error, $i, 'Indent', $data); 496 if ($fix === true) { 497 $padding = str_repeat(' ', $expectedIndent); 498 if ($foundIndent === 0) { 499 $phpcsFile->fixer->addContentBefore($i, $padding); 500 } else { 501 if ($tokens[$i]['code'] === T_COMMENT) { 502 $comment = $padding.ltrim($tokens[$i]['content']); 503 $phpcsFile->fixer->replaceToken($i, $comment); 504 } else { 505 $phpcsFile->fixer->replaceToken($i, $padding); 506 } 507 } 508 } 509 }//end if 510 } else { 511 $nextCode = $i; 512 }//end if 513 514 if ($inArg === false) { 515 $argStart = $nextCode; 516 $argEnd = $phpcsFile->findEndOfStatement($nextCode); 517 } 518 }//end if 519 520 // If we are within an argument we should be ignoring commas 521 // as these are not signaling the end of an argument. 522 if ($inArg === false && $tokens[$i]['code'] === T_COMMA) { 523 $next = $phpcsFile->findNext(array(T_WHITESPACE, T_COMMENT), ($i + 1), $closeBracket, true); 524 if ($next === false) { 525 continue; 526 } 527 528 if ($this->allowMultipleArguments === false) { 529 // Comma has to be the last token on the line. 530 if ($tokens[$i]['line'] === $tokens[$next]['line']) { 531 $error = 'Only one argument is allowed per line in a multi-line function call'; 532 $fix = $phpcsFile->addFixableError($error, $next, 'MultipleArguments'); 533 if ($fix === true) { 534 $phpcsFile->fixer->beginChangeset(); 535 for ($x = ($next - 1); $x > $i; $x--) { 536 if ($tokens[$x]['code'] !== T_WHITESPACE) { 537 break; 538 } 539 540 $phpcsFile->fixer->replaceToken($x, ''); 541 } 542 543 $phpcsFile->fixer->addContentBefore( 544 $next, 545 $phpcsFile->eolChar.str_repeat(' ', ($functionIndent + $this->indent)) 546 ); 547 $phpcsFile->fixer->endChangeset(); 548 } 549 } 550 }//end if 551 552 $argStart = $next; 553 $argEnd = $phpcsFile->findEndOfStatement($next); 554 }//end if 555 }//end for 556 557 }//end processMultiLineCall() 558 559 560}//end class 561