1<?php 2/** 3 * Class Declaration Test. 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 15if (class_exists('PEAR_Sniffs_Classes_ClassDeclarationSniff', true) === false) { 16 $error = 'Class PEAR_Sniffs_Classes_ClassDeclarationSniff not found'; 17 throw new PHP_CodeSniffer_Exception($error); 18} 19 20/** 21 * Class Declaration Test. 22 * 23 * Checks the declaration of the class and its inheritance is correct. 24 * 25 * @category PHP 26 * @package PHP_CodeSniffer 27 * @author Greg Sherwood <gsherwood@squiz.net> 28 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 29 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 30 * @version Release: @package_version@ 31 * @link http://pear.php.net/package/PHP_CodeSniffer 32 */ 33class PSR2_Sniffs_Classes_ClassDeclarationSniff extends PEAR_Sniffs_Classes_ClassDeclarationSniff 34{ 35 36 37 /** 38 * Processes this test, when one of its tokens is encountered. 39 * 40 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 41 * @param int $stackPtr The position of the current token 42 * in the stack passed in $tokens. 43 * 44 * @return void 45 */ 46 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 47 { 48 // We want all the errors from the PEAR standard, plus some of our own. 49 parent::process($phpcsFile, $stackPtr); 50 51 // Just in case. 52 $tokens = $phpcsFile->getTokens(); 53 if (isset($tokens[$stackPtr]['scope_opener']) === false) { 54 return; 55 } 56 57 $this->processOpen($phpcsFile, $stackPtr); 58 $this->processClose($phpcsFile, $stackPtr); 59 60 }//end process() 61 62 63 /** 64 * Processes the opening section of a class declaration. 65 * 66 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 67 * @param int $stackPtr The position of the current token 68 * in the stack passed in $tokens. 69 * 70 * @return void 71 */ 72 public function processOpen(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 73 { 74 $tokens = $phpcsFile->getTokens(); 75 $stackPtrType = strtolower($tokens[$stackPtr]['content']); 76 77 // Check alignment of the keyword and braces. 78 if ($tokens[($stackPtr - 1)]['code'] === T_WHITESPACE) { 79 $prevContent = $tokens[($stackPtr - 1)]['content']; 80 if ($prevContent !== $phpcsFile->eolChar) { 81 $blankSpace = substr($prevContent, strpos($prevContent, $phpcsFile->eolChar)); 82 $spaces = strlen($blankSpace); 83 84 if (in_array($tokens[($stackPtr - 2)]['code'], array(T_ABSTRACT, T_FINAL)) === true 85 && $spaces !== 1 86 ) { 87 $prevContent = strtolower($tokens[($stackPtr - 2)]['content']); 88 $error = 'Expected 1 space between %s and %s keywords; %s found'; 89 $data = array( 90 $prevContent, 91 $stackPtrType, 92 $spaces, 93 ); 94 95 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeKeyword', $data); 96 if ($fix === true) { 97 $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' '); 98 } 99 } 100 } else if ($tokens[($stackPtr - 2)]['code'] === T_ABSTRACT 101 || $tokens[($stackPtr - 2)]['code'] === T_FINAL 102 ) { 103 $prevContent = strtolower($tokens[($stackPtr - 2)]['content']); 104 $error = 'Expected 1 space between %s and %s keywords; newline found'; 105 $data = array( 106 $prevContent, 107 $stackPtrType, 108 ); 109 110 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NewlineBeforeKeyword', $data); 111 if ($fix === true) { 112 $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' '); 113 } 114 }//end if 115 }//end if 116 117 // We'll need the indent of the class/interface declaration for later. 118 $classIndent = 0; 119 for ($i = ($stackPtr - 1); $i > 0; $i--) { 120 if ($tokens[$i]['line'] === $tokens[$stackPtr]['line']) { 121 continue; 122 } 123 124 // We changed lines. 125 if ($tokens[($i + 1)]['code'] === T_WHITESPACE) { 126 $classIndent = strlen($tokens[($i + 1)]['content']); 127 } 128 129 break; 130 } 131 132 $className = $phpcsFile->findNext(T_STRING, $stackPtr); 133 134 // Spacing of the keyword. 135 $gap = $tokens[($stackPtr + 1)]['content']; 136 if (strlen($gap) !== 1) { 137 $found = strlen($gap); 138 $error = 'Expected 1 space between %s keyword and %s name; %s found'; 139 $data = array( 140 $stackPtrType, 141 $stackPtrType, 142 $found, 143 ); 144 145 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterKeyword', $data); 146 if ($fix === true) { 147 $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' '); 148 } 149 } 150 151 // Check after the class/interface name. 152 if ($tokens[($className + 2)]['line'] === $tokens[$className]['line']) { 153 $gap = $tokens[($className + 1)]['content']; 154 if (strlen($gap) !== 1) { 155 $found = strlen($gap); 156 $error = 'Expected 1 space after %s name; %s found'; 157 $data = array( 158 $stackPtrType, 159 $found, 160 ); 161 162 $fix = $phpcsFile->addFixableError($error, $className, 'SpaceAfterName', $data); 163 if ($fix === true) { 164 $phpcsFile->fixer->replaceToken(($className + 1), ' '); 165 } 166 } 167 } 168 169 $openingBrace = $tokens[$stackPtr]['scope_opener']; 170 171 // Check positions of the extends and implements keywords. 172 foreach (array('extends', 'implements') as $keywordType) { 173 $keyword = $phpcsFile->findNext(constant('T_'.strtoupper($keywordType)), ($stackPtr + 1), $openingBrace); 174 if ($keyword !== false) { 175 if ($tokens[$keyword]['line'] !== $tokens[$stackPtr]['line']) { 176 $error = 'The '.$keywordType.' keyword must be on the same line as the %s name'; 177 $data = array($stackPtrType); 178 $fix = $phpcsFile->addFixableError($error, $keyword, ucfirst($keywordType).'Line', $data); 179 if ($fix === true) { 180 $phpcsFile->fixer->beginChangeset(); 181 for ($i = ($stackPtr + 1); $i < $keyword; $i++) { 182 if ($tokens[$i]['line'] !== $tokens[($i + 1)]['line']) { 183 $phpcsFile->fixer->substrToken($i, 0, (strlen($phpcsFile->eolChar) * -1)); 184 } 185 } 186 187 $phpcsFile->fixer->addContentBefore($keyword, ' '); 188 $phpcsFile->fixer->endChangeset(); 189 } 190 } else { 191 // Check the whitespace before. Whitespace after is checked 192 // later by looking at the whitespace before the first class name 193 // in the list. 194 $gap = strlen($tokens[($keyword - 1)]['content']); 195 if ($gap !== 1) { 196 $error = 'Expected 1 space before '.$keywordType.' keyword; %s found'; 197 $data = array($gap); 198 $fix = $phpcsFile->addFixableError($error, $keyword, 'SpaceBefore'.ucfirst($keywordType), $data); 199 if ($fix === true) { 200 $phpcsFile->fixer->replaceToken(($keyword - 1), ' '); 201 } 202 } 203 }//end if 204 }//end if 205 }//end foreach 206 207 // Check each of the extends/implements class names. If the extends/implements 208 // keyword is the last content on the line, it means we need to check for 209 // the multi-line format, so we do not include the class names 210 // from the extends/implements list in the following check. 211 // Note that classes can only extend one other class, so they can't use a 212 // multi-line extends format, whereas an interface can extend multiple 213 // other interfaces, and so uses a multi-line extends format. 214 if ($tokens[$stackPtr]['code'] === T_INTERFACE) { 215 $keywordTokenType = T_EXTENDS; 216 } else { 217 $keywordTokenType = T_IMPLEMENTS; 218 } 219 220 $implements = $phpcsFile->findNext($keywordTokenType, ($stackPtr + 1), $openingBrace); 221 $multiLineImplements = false; 222 if ($implements !== false) { 223 $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($openingBrace - 1), $implements, true); 224 if ($tokens[$prev]['line'] !== $tokens[$implements]['line']) { 225 $multiLineImplements = true; 226 } 227 } 228 229 $find = array( 230 T_STRING, 231 $keywordTokenType, 232 ); 233 234 $classNames = array(); 235 $nextClass = $phpcsFile->findNext($find, ($className + 2), ($openingBrace - 1)); 236 while ($nextClass !== false) { 237 $classNames[] = $nextClass; 238 $nextClass = $phpcsFile->findNext($find, ($nextClass + 1), ($openingBrace - 1)); 239 } 240 241 $classCount = count($classNames); 242 $checkingImplements = false; 243 $implementsToken = null; 244 foreach ($classNames as $i => $className) { 245 if ($tokens[$className]['code'] === $keywordTokenType) { 246 $checkingImplements = true; 247 $implementsToken = $className; 248 continue; 249 } 250 251 if ($checkingImplements === true 252 && $multiLineImplements === true 253 && ($tokens[($className - 1)]['code'] !== T_NS_SEPARATOR 254 || $tokens[($className - 2)]['code'] !== T_STRING) 255 ) { 256 $prev = $phpcsFile->findPrevious( 257 array( 258 T_NS_SEPARATOR, 259 T_WHITESPACE, 260 ), 261 ($className - 1), 262 $implements, 263 true 264 ); 265 266 if ($prev === $implementsToken && $tokens[$className]['line'] !== ($tokens[$prev]['line'] + 1)) { 267 if ($keywordTokenType === T_EXTENDS) { 268 $error = 'The first item in a multi-line extends list must be on the line following the extends keyword'; 269 $fix = $phpcsFile->addFixableError($error, $className, 'FirstExtendsInterfaceSameLine'); 270 } else { 271 $error = 'The first item in a multi-line implements list must be on the line following the implements keyword'; 272 $fix = $phpcsFile->addFixableError($error, $className, 'FirstInterfaceSameLine'); 273 } 274 275 if ($fix === true) { 276 $phpcsFile->fixer->beginChangeset(); 277 for ($i = ($prev + 1); $i < $className; $i++) { 278 if ($tokens[$i]['code'] !== T_WHITESPACE) { 279 break; 280 } 281 282 $phpcsFile->fixer->replaceToken($i, ''); 283 } 284 285 $phpcsFile->fixer->addNewline($prev); 286 $phpcsFile->fixer->endChangeset(); 287 } 288 } else if ($tokens[$prev]['line'] !== ($tokens[$className]['line'] - 1)) { 289 if ($keywordTokenType === T_EXTENDS) { 290 $error = 'Only one interface may be specified per line in a multi-line extends declaration'; 291 $fix = $phpcsFile->addFixableError($error, $className, 'ExtendsInterfaceSameLine'); 292 } else { 293 $error = 'Only one interface may be specified per line in a multi-line implements declaration'; 294 $fix = $phpcsFile->addFixableError($error, $className, 'InterfaceSameLine'); 295 } 296 297 if ($fix === true) { 298 $phpcsFile->fixer->beginChangeset(); 299 for ($i = ($prev + 1); $i < $className; $i++) { 300 if ($tokens[$i]['code'] !== T_WHITESPACE) { 301 break; 302 } 303 304 $phpcsFile->fixer->replaceToken($i, ''); 305 } 306 307 $phpcsFile->fixer->addNewline($prev); 308 $phpcsFile->fixer->endChangeset(); 309 } 310 } else { 311 $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($className - 1), $implements); 312 if ($tokens[$prev]['line'] !== $tokens[$className]['line']) { 313 $found = 0; 314 } else { 315 $found = strlen($tokens[$prev]['content']); 316 } 317 318 $expected = ($classIndent + $this->indent); 319 if ($found !== $expected) { 320 $error = 'Expected %s spaces before interface name; %s found'; 321 $data = array( 322 $expected, 323 $found, 324 ); 325 $fix = $phpcsFile->addFixableError($error, $className, 'InterfaceWrongIndent', $data); 326 if ($fix === true) { 327 $padding = str_repeat(' ', $expected); 328 if ($found === 0) { 329 $phpcsFile->fixer->addContent($prev, $padding); 330 } else { 331 $phpcsFile->fixer->replaceToken($prev, $padding); 332 } 333 } 334 } 335 }//end if 336 } else if ($tokens[($className - 1)]['code'] !== T_NS_SEPARATOR 337 || $tokens[($className - 2)]['code'] !== T_STRING 338 ) { 339 // Not part of a longer fully qualified class name. 340 if ($tokens[($className - 1)]['code'] === T_COMMA 341 || ($tokens[($className - 1)]['code'] === T_NS_SEPARATOR 342 && $tokens[($className - 2)]['code'] === T_COMMA) 343 ) { 344 $error = 'Expected 1 space before "%s"; 0 found'; 345 $data = array($tokens[$className]['content']); 346 $fix = $phpcsFile->addFixableError($error, ($nextComma + 1), 'NoSpaceBeforeName', $data); 347 if ($fix === true) { 348 $phpcsFile->fixer->addContentBefore(($nextComma + 1), ' '); 349 } 350 } else { 351 if ($tokens[($className - 1)]['code'] === T_NS_SEPARATOR) { 352 $prev = ($className - 2); 353 } else { 354 $prev = ($className - 1); 355 } 356 357 $spaceBefore = strlen($tokens[$prev]['content']); 358 if ($spaceBefore !== 1) { 359 $error = 'Expected 1 space before "%s"; %s found'; 360 $data = array( 361 $tokens[$className]['content'], 362 $spaceBefore, 363 ); 364 365 $fix = $phpcsFile->addFixableError($error, $className, 'SpaceBeforeName', $data); 366 if ($fix === true) { 367 $phpcsFile->fixer->replaceToken($prev, ' '); 368 } 369 } 370 }//end if 371 }//end if 372 373 if ($checkingImplements === true 374 && $tokens[($className + 1)]['code'] !== T_NS_SEPARATOR 375 && $tokens[($className + 1)]['code'] !== T_COMMA 376 ) { 377 if ($i !== ($classCount - 1)) { 378 // This is not the last class name, and the comma 379 // is not where we expect it to be. 380 if ($tokens[($className + 2)]['code'] !== $keywordTokenType) { 381 $error = 'Expected 0 spaces between "%s" and comma; %s found'; 382 $data = array( 383 $tokens[$className]['content'], 384 strlen($tokens[($className + 1)]['content']), 385 ); 386 387 $fix = $phpcsFile->addFixableError($error, $className, 'SpaceBeforeComma', $data); 388 if ($fix === true) { 389 $phpcsFile->fixer->replaceToken(($className + 1), ''); 390 } 391 } 392 } 393 394 $nextComma = $phpcsFile->findNext(T_COMMA, $className); 395 } else { 396 $nextComma = ($className + 1); 397 }//end if 398 }//end foreach 399 400 }//end processOpen() 401 402 403 /** 404 * Processes the closing section of a class declaration. 405 * 406 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 407 * @param int $stackPtr The position of the current token 408 * in the stack passed in $tokens. 409 * 410 * @return void 411 */ 412 public function processClose(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 413 { 414 $tokens = $phpcsFile->getTokens(); 415 416 // Check that the closing brace comes right after the code body. 417 $closeBrace = $tokens[$stackPtr]['scope_closer']; 418 $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($closeBrace - 1), null, true); 419 if ($prevContent !== $tokens[$stackPtr]['scope_opener'] 420 && $tokens[$prevContent]['line'] !== ($tokens[$closeBrace]['line'] - 1) 421 ) { 422 $error = 'The closing brace for the %s must go on the next line after the body'; 423 $data = array($tokens[$stackPtr]['content']); 424 $fix = $phpcsFile->addFixableError($error, $closeBrace, 'CloseBraceAfterBody', $data); 425 426 if ($fix === true) { 427 $phpcsFile->fixer->beginChangeset(); 428 for ($i = ($prevContent + 1); $i < $closeBrace; $i++) { 429 $phpcsFile->fixer->replaceToken($i, ''); 430 } 431 432 if (strpos($tokens[$prevContent]['content'], $phpcsFile->eolChar) === false) { 433 $phpcsFile->fixer->replaceToken($closeBrace, $phpcsFile->eolChar.$tokens[$closeBrace]['content']); 434 } 435 436 $phpcsFile->fixer->endChangeset(); 437 } 438 }//end if 439 440 // Check the closing brace is on it's own line, but allow 441 // for comments like "//end class". 442 $nextContent = $phpcsFile->findNext(array(T_WHITESPACE, T_COMMENT), ($closeBrace + 1), null, true); 443 if ($tokens[$nextContent]['content'] !== $phpcsFile->eolChar 444 && $tokens[$nextContent]['line'] === $tokens[$closeBrace]['line'] 445 ) { 446 $type = strtolower($tokens[$stackPtr]['content']); 447 $error = 'Closing %s brace must be on a line by itself'; 448 $data = array($type); 449 $phpcsFile->addError($error, $closeBrace, 'CloseBraceSameLine', $data); 450 } 451 452 }//end processClose() 453 454 455}//end class 456