1<?php 2/** 3 * Checks the separation between functions and methods. 4 * 5 * @author Greg Sherwood <gsherwood@squiz.net> 6 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) 7 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 8 */ 9 10namespace PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace; 11 12use PHP_CodeSniffer\Files\File; 13use PHP_CodeSniffer\Sniffs\Sniff; 14use PHP_CodeSniffer\Util\Tokens; 15 16class FunctionSpacingSniff implements Sniff 17{ 18 19 /** 20 * The number of blank lines between functions. 21 * 22 * @var integer 23 */ 24 public $spacing = 2; 25 26 /** 27 * The number of blank lines before the first function in a class. 28 * 29 * @var integer 30 */ 31 public $spacingBeforeFirst = 2; 32 33 /** 34 * The number of blank lines after the last function in a class. 35 * 36 * @var integer 37 */ 38 public $spacingAfterLast = 2; 39 40 /** 41 * Original properties as set in a custom ruleset (if any). 42 * 43 * @var array|null 44 */ 45 private $rulesetProperties = null; 46 47 48 /** 49 * Returns an array of tokens this test wants to listen for. 50 * 51 * @return array 52 */ 53 public function register() 54 { 55 return [T_FUNCTION]; 56 57 }//end register() 58 59 60 /** 61 * Processes this sniff when one of its tokens is encountered. 62 * 63 * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. 64 * @param int $stackPtr The position of the current token 65 * in the stack passed in $tokens. 66 * 67 * @return void 68 */ 69 public function process(File $phpcsFile, $stackPtr) 70 { 71 $tokens = $phpcsFile->getTokens(); 72 $previousNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); 73 if ($previousNonEmpty !== false 74 && $tokens[$previousNonEmpty]['code'] === T_OPEN_TAG 75 && $tokens[$previousNonEmpty]['line'] !== 1 76 ) { 77 // Ignore functions at the start of an embedded PHP block. 78 return; 79 } 80 81 // If the ruleset has only overridden the spacing property, use 82 // that value for all spacing rules. 83 if ($this->rulesetProperties === null) { 84 $this->rulesetProperties = []; 85 if (isset($phpcsFile->ruleset->ruleset['Squiz.WhiteSpace.FunctionSpacing']) === true 86 && isset($phpcsFile->ruleset->ruleset['Squiz.WhiteSpace.FunctionSpacing']['properties']) === true 87 ) { 88 $this->rulesetProperties = $phpcsFile->ruleset->ruleset['Squiz.WhiteSpace.FunctionSpacing']['properties']; 89 if (isset($this->rulesetProperties['spacing']) === true) { 90 if (isset($this->rulesetProperties['spacingBeforeFirst']) === false) { 91 $this->spacingBeforeFirst = $this->spacing; 92 } 93 94 if (isset($this->rulesetProperties['spacingAfterLast']) === false) { 95 $this->spacingAfterLast = $this->spacing; 96 } 97 } 98 } 99 } 100 101 $this->spacing = (int) $this->spacing; 102 $this->spacingBeforeFirst = (int) $this->spacingBeforeFirst; 103 $this->spacingAfterLast = (int) $this->spacingAfterLast; 104 105 if (isset($tokens[$stackPtr]['scope_closer']) === false) { 106 // Must be an interface method, so the closer is the semicolon. 107 $closer = $phpcsFile->findNext(T_SEMICOLON, $stackPtr); 108 } else { 109 $closer = $tokens[$stackPtr]['scope_closer']; 110 } 111 112 $isFirst = false; 113 $isLast = false; 114 115 $ignore = ([T_WHITESPACE => T_WHITESPACE] + Tokens::$methodPrefixes); 116 117 $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); 118 119 while ($tokens[$prev]['code'] === T_ATTRIBUTE_END) { 120 // Skip past function attributes. 121 $prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['attribute_opener'] - 1), null, true); 122 } 123 124 if ($tokens[$prev]['code'] === T_DOC_COMMENT_CLOSE_TAG) { 125 // Skip past function docblocks. 126 $prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['comment_opener'] - 1), null, true); 127 } 128 129 if ($tokens[$prev]['code'] === T_OPEN_CURLY_BRACKET) { 130 $isFirst = true; 131 } 132 133 $next = $phpcsFile->findNext($ignore, ($closer + 1), null, true); 134 if (isset(Tokens::$emptyTokens[$tokens[$next]['code']]) === true 135 && $tokens[$next]['line'] === $tokens[$closer]['line'] 136 ) { 137 // Skip past "end" comments. 138 $next = $phpcsFile->findNext($ignore, ($next + 1), null, true); 139 } 140 141 if ($tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET) { 142 $isLast = true; 143 } 144 145 /* 146 Check the number of blank lines 147 after the function. 148 */ 149 150 // Allow for comments on the same line as the closer. 151 for ($nextLineToken = ($closer + 1); $nextLineToken < $phpcsFile->numTokens; $nextLineToken++) { 152 if ($tokens[$nextLineToken]['line'] !== $tokens[$closer]['line']) { 153 break; 154 } 155 } 156 157 $requiredSpacing = $this->spacing; 158 $errorCode = 'After'; 159 if ($isLast === true) { 160 $requiredSpacing = $this->spacingAfterLast; 161 $errorCode = 'AfterLast'; 162 } 163 164 $foundLines = 0; 165 if ($nextLineToken === ($phpcsFile->numTokens - 1)) { 166 // We are at the end of the file. 167 // Don't check spacing after the function because this 168 // should be done by an EOF sniff. 169 $foundLines = $requiredSpacing; 170 } else { 171 $nextContent = $phpcsFile->findNext(T_WHITESPACE, $nextLineToken, null, true); 172 if ($nextContent === false) { 173 // We are at the end of the file. 174 // Don't check spacing after the function because this 175 // should be done by an EOF sniff. 176 $foundLines = $requiredSpacing; 177 } else { 178 $foundLines = ($tokens[$nextContent]['line'] - $tokens[$nextLineToken]['line']); 179 } 180 } 181 182 if ($isLast === true) { 183 $phpcsFile->recordMetric($stackPtr, 'Function spacing after last', $foundLines); 184 } else { 185 $phpcsFile->recordMetric($stackPtr, 'Function spacing after', $foundLines); 186 } 187 188 if ($foundLines !== $requiredSpacing) { 189 $error = 'Expected %s blank line'; 190 if ($requiredSpacing !== 1) { 191 $error .= 's'; 192 } 193 194 $error .= ' after function; %s found'; 195 $data = [ 196 $requiredSpacing, 197 $foundLines, 198 ]; 199 200 $fix = $phpcsFile->addFixableError($error, $closer, $errorCode, $data); 201 if ($fix === true) { 202 $phpcsFile->fixer->beginChangeset(); 203 for ($i = $nextLineToken; $i <= $nextContent; $i++) { 204 if ($tokens[$i]['line'] === $tokens[$nextContent]['line']) { 205 $phpcsFile->fixer->addContentBefore($i, str_repeat($phpcsFile->eolChar, $requiredSpacing)); 206 break; 207 } 208 209 $phpcsFile->fixer->replaceToken($i, ''); 210 } 211 212 $phpcsFile->fixer->endChangeset(); 213 }//end if 214 }//end if 215 216 /* 217 Check the number of blank lines 218 before the function. 219 */ 220 221 $prevLineToken = null; 222 for ($i = $stackPtr; $i >= 0; $i--) { 223 if ($tokens[$i]['line'] === $tokens[$stackPtr]['line']) { 224 continue; 225 } 226 227 $prevLineToken = $i; 228 break; 229 } 230 231 if ($prevLineToken === null) { 232 // Never found the previous line, which means 233 // there are 0 blank lines before the function. 234 $foundLines = 0; 235 $prevContent = 0; 236 $prevLineToken = 0; 237 } else { 238 $currentLine = $tokens[$stackPtr]['line']; 239 240 $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, $prevLineToken, null, true); 241 242 if ($tokens[$prevContent]['code'] === T_COMMENT 243 || isset(Tokens::$phpcsCommentTokens[$tokens[$prevContent]['code']]) === true 244 ) { 245 // Ignore comments as they can have different spacing rules, and this 246 // isn't a proper function comment anyway. 247 return; 248 } 249 250 while ($tokens[$prevContent]['code'] === T_ATTRIBUTE_END 251 && $tokens[$prevContent]['line'] === ($currentLine - 1) 252 ) { 253 // Account for function attributes. 254 $currentLine = $tokens[$tokens[$prevContent]['attribute_opener']]['line']; 255 $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$prevContent]['attribute_opener'] - 1), null, true); 256 } 257 258 if ($tokens[$prevContent]['code'] === T_DOC_COMMENT_CLOSE_TAG 259 && $tokens[$prevContent]['line'] === ($currentLine - 1) 260 ) { 261 // Account for function comments. 262 $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$prevContent]['comment_opener'] - 1), null, true); 263 } 264 265 $prevLineToken = $prevContent; 266 267 // Before we throw an error, check that we are not throwing an error 268 // for another function. We don't want to error for no blank lines after 269 // the previous function and no blank lines before this one as well. 270 $prevLine = ($tokens[$prevContent]['line'] - 1); 271 $i = ($stackPtr - 1); 272 $foundLines = 0; 273 274 $stopAt = 0; 275 if (isset($tokens[$stackPtr]['conditions']) === true) { 276 $conditions = $tokens[$stackPtr]['conditions']; 277 $conditions = array_keys($conditions); 278 $stopAt = array_pop($conditions); 279 } 280 281 while ($currentLine !== $prevLine && $currentLine > 1 && $i > $stopAt) { 282 if ($tokens[$i]['code'] === T_FUNCTION) { 283 // Found another interface or abstract function. 284 return; 285 } 286 287 if ($tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET 288 && $tokens[$tokens[$i]['scope_condition']]['code'] === T_FUNCTION 289 ) { 290 // Found a previous function. 291 return; 292 } 293 294 $currentLine = $tokens[$i]['line']; 295 if ($currentLine === $prevLine) { 296 break; 297 } 298 299 if ($tokens[($i - 1)]['line'] < $currentLine && $tokens[($i + 1)]['line'] > $currentLine) { 300 // This token is on a line by itself. If it is whitespace, the line is empty. 301 if ($tokens[$i]['code'] === T_WHITESPACE) { 302 $foundLines++; 303 } 304 } 305 306 $i--; 307 }//end while 308 }//end if 309 310 $requiredSpacing = $this->spacing; 311 $errorCode = 'Before'; 312 if ($isFirst === true) { 313 $requiredSpacing = $this->spacingBeforeFirst; 314 $errorCode = 'BeforeFirst'; 315 316 $phpcsFile->recordMetric($stackPtr, 'Function spacing before first', $foundLines); 317 } else { 318 $phpcsFile->recordMetric($stackPtr, 'Function spacing before', $foundLines); 319 } 320 321 if ($foundLines !== $requiredSpacing) { 322 $error = 'Expected %s blank line'; 323 if ($requiredSpacing !== 1) { 324 $error .= 's'; 325 } 326 327 $error .= ' before function; %s found'; 328 $data = [ 329 $requiredSpacing, 330 $foundLines, 331 ]; 332 333 $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data); 334 if ($fix === true) { 335 $nextSpace = $phpcsFile->findNext(T_WHITESPACE, ($prevContent + 1), $stackPtr); 336 if ($nextSpace === false) { 337 $nextSpace = ($stackPtr - 1); 338 } 339 340 if ($foundLines < $requiredSpacing) { 341 $padding = str_repeat($phpcsFile->eolChar, ($requiredSpacing - $foundLines)); 342 $phpcsFile->fixer->addContent($prevLineToken, $padding); 343 } else { 344 $nextContent = $phpcsFile->findNext(T_WHITESPACE, ($nextSpace + 1), null, true); 345 $phpcsFile->fixer->beginChangeset(); 346 for ($i = $nextSpace; $i < $nextContent; $i++) { 347 if ($tokens[$i]['line'] === $tokens[$prevContent]['line']) { 348 continue; 349 } 350 351 if ($tokens[$i]['line'] === $tokens[$nextContent]['line']) { 352 $phpcsFile->fixer->addContentBefore($i, str_repeat($phpcsFile->eolChar, $requiredSpacing)); 353 break; 354 } 355 356 $phpcsFile->fixer->replaceToken($i, ''); 357 } 358 359 $phpcsFile->fixer->endChangeset(); 360 }//end if 361 }//end if 362 }//end if 363 364 }//end process() 365 366 367}//end class 368