1<?php 2/** 3 * Squiz_Sniffs_PHP_EmbeddedPhpSniff. 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 * Squiz_Sniffs_PHP_EmbeddedPhpSniff. 18 * 19 * Checks the indentation of embedded PHP code segments. 20 * 21 * @category PHP 22 * @package PHP_CodeSniffer 23 * @author Greg Sherwood <gsherwood@squiz.net> 24 * @author Marc McIntyre <mmcintyre@squiz.net> 25 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 26 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 27 * @version Release: @package_version@ 28 * @link http://pear.php.net/package/PHP_CodeSniffer 29 */ 30class Squiz_Sniffs_PHP_EmbeddedPhpSniff implements PHP_CodeSniffer_Sniff 31{ 32 33 34 /** 35 * Returns an array of tokens this test wants to listen for. 36 * 37 * @return array 38 */ 39 public function register() 40 { 41 return array(T_OPEN_TAG); 42 43 }//end register() 44 45 46 /** 47 * Processes this test, when one of its tokens is encountered. 48 * 49 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 50 * @param int $stackPtr The position of the current token in the 51 * stack passed in $tokens. 52 * 53 * @return void 54 */ 55 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 56 { 57 $tokens = $phpcsFile->getTokens(); 58 59 // If the close php tag is on the same line as the opening 60 // then we have an inline embedded PHP block. 61 $closeTag = $phpcsFile->findNext(T_CLOSE_TAG, $stackPtr); 62 if ($closeTag === false || $tokens[$stackPtr]['line'] !== $tokens[$closeTag]['line']) { 63 $this->_validateMultilineEmbeddedPhp($phpcsFile, $stackPtr); 64 } else { 65 $this->_validateInlineEmbeddedPhp($phpcsFile, $stackPtr); 66 } 67 68 }//end process() 69 70 71 /** 72 * Validates embedded PHP that exists on multiple lines. 73 * 74 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 75 * @param int $stackPtr The position of the current token in the 76 * stack passed in $tokens. 77 * 78 * @return void 79 */ 80 private function _validateMultilineEmbeddedPhp(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 81 { 82 $tokens = $phpcsFile->getTokens(); 83 84 $prevTag = $phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1)); 85 if ($prevTag === false) { 86 // This is the first open tag. 87 return; 88 } 89 90 $firstContent = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); 91 $closingTag = $phpcsFile->findNext(T_CLOSE_TAG, $stackPtr); 92 if ($closingTag !== false) { 93 $nextContent = $phpcsFile->findNext(T_WHITESPACE, ($closingTag + 1), $phpcsFile->numTokens, true); 94 if ($nextContent === false) { 95 // Final closing tag. It will be handled elsewhere. 96 return; 97 } 98 99 // We have an opening and a closing tag, that lie within other content. 100 if ($firstContent === $closingTag) { 101 $error = 'Empty embedded PHP tag found'; 102 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Empty'); 103 if ($fix === true) { 104 $phpcsFile->fixer->beginChangeset(); 105 for ($i = $stackPtr; $i <= $closingTag; $i++) { 106 $phpcsFile->fixer->replaceToken($i, ''); 107 } 108 109 $phpcsFile->fixer->endChangeset(); 110 } 111 112 return; 113 } 114 }//end if 115 116 if ($tokens[$firstContent]['line'] === $tokens[$stackPtr]['line']) { 117 $error = 'Opening PHP tag must be on a line by itself'; 118 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ContentAfterOpen'); 119 if ($fix === true) { 120 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr, true); 121 $padding = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content']))); 122 $phpcsFile->fixer->beginChangeset(); 123 $phpcsFile->fixer->addNewline($stackPtr); 124 $phpcsFile->fixer->addContent($stackPtr, str_repeat(' ', $padding)); 125 $phpcsFile->fixer->endChangeset(); 126 } 127 } else { 128 // Check the indent of the first line, except if it is a scope closer. 129 if (isset($tokens[$firstContent]['scope_closer']) === false 130 || $tokens[$firstContent]['scope_closer'] !== $firstContent 131 ) { 132 // Check for a blank line at the top. 133 if ($tokens[$firstContent]['line'] > ($tokens[$stackPtr]['line'] + 1)) { 134 // Find a token on the blank line to throw the error on. 135 $i = $stackPtr; 136 do { 137 $i++; 138 } while ($tokens[$i]['line'] !== ($tokens[$stackPtr]['line'] + 1)); 139 140 $error = 'Blank line found at start of embedded PHP content'; 141 $fix = $phpcsFile->addFixableError($error, $i, 'SpacingBefore'); 142 if ($fix === true) { 143 $phpcsFile->fixer->beginChangeset(); 144 for ($i = ($stackPtr + 1); $i < $firstContent; $i++) { 145 if ($tokens[$i]['line'] === $tokens[$firstContent]['line'] 146 || $tokens[$i]['line'] === $tokens[$stackPtr]['line'] 147 ) { 148 continue; 149 } 150 151 $phpcsFile->fixer->replaceToken($i, ''); 152 } 153 154 $phpcsFile->fixer->endChangeset(); 155 } 156 }//end if 157 158 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr); 159 if ($first === false) { 160 $first = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr); 161 $indent = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content']))); 162 } else { 163 $indent = ($tokens[($first + 1)]['column'] - 1); 164 } 165 166 $contentColumn = ($tokens[$firstContent]['column'] - 1); 167 if ($contentColumn !== $indent) { 168 $error = 'First line of embedded PHP code must be indented %s spaces; %s found'; 169 $data = array( 170 $indent, 171 $contentColumn, 172 ); 173 $fix = $phpcsFile->addFixableError($error, $firstContent, 'Indent', $data); 174 if ($fix === true) { 175 $padding = str_repeat(' ', $indent); 176 if ($contentColumn === 0) { 177 $phpcsFile->fixer->addContentBefore($firstContent, $padding); 178 } else { 179 $phpcsFile->fixer->replaceToken(($firstContent - 1), $padding); 180 } 181 } 182 } 183 }//end if 184 }//end if 185 186 $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); 187 if ($tokens[$lastContent]['line'] === $tokens[$stackPtr]['line'] 188 && trim($tokens[$lastContent]['content']) !== '' 189 ) { 190 $error = 'Opening PHP tag must be on a line by itself'; 191 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ContentBeforeOpen'); 192 if ($fix === true) { 193 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr); 194 if ($first === false) { 195 $first = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr); 196 $padding = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content']))); 197 } else { 198 $padding = ($tokens[($first + 1)]['column'] - 1); 199 } 200 201 $phpcsFile->fixer->addContentBefore($stackPtr, $phpcsFile->eolChar.str_repeat(' ', $padding)); 202 } 203 } else { 204 // Find the first token on the first non-empty line we find. 205 for ($first = ($stackPtr - 1); $first > 0; $first--) { 206 if ($tokens[$first]['line'] === $tokens[$stackPtr]['line']) { 207 continue; 208 } else if (trim($tokens[$first]['content']) !== '') { 209 $first = $phpcsFile->findFirstOnLine(array(), $first, true); 210 break; 211 } 212 } 213 214 $expected = 0; 215 if ($tokens[$first]['code'] === T_INLINE_HTML 216 && trim($tokens[$first]['content']) !== '' 217 ) { 218 $expected = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content']))); 219 } else if ($tokens[$first]['code'] === T_WHITESPACE) { 220 $expected = ($tokens[($first + 1)]['column'] - 1); 221 } 222 223 $expected += 4; 224 $found = ($tokens[$stackPtr]['column'] - 1); 225 if ($found > $expected) { 226 $error = 'Opening PHP tag indent incorrect; expected no more than %s spaces but found %s'; 227 $data = array( 228 $expected, 229 $found, 230 ); 231 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'OpenTagIndent', $data); 232 if ($fix === true) { 233 $phpcsFile->fixer->replaceToken(($stackPtr - 1), str_repeat(' ', $expected)); 234 } 235 } 236 }//end if 237 238 if ($closingTag === false) { 239 return; 240 } 241 242 $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($closingTag - 1), ($stackPtr + 1), true); 243 $nextContent = $phpcsFile->findNext(T_WHITESPACE, ($closingTag + 1), null, true); 244 245 if ($tokens[$lastContent]['line'] === $tokens[$closingTag]['line']) { 246 $error = 'Closing PHP tag must be on a line by itself'; 247 $fix = $phpcsFile->addFixableError($error, $closingTag, 'ContentBeforeEnd'); 248 if ($fix === true) { 249 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $closingTag, true); 250 $phpcsFile->fixer->beginChangeset(); 251 $phpcsFile->fixer->addContentBefore($closingTag, str_repeat(' ', ($tokens[$first]['column'] - 1))); 252 $phpcsFile->fixer->addNewlineBefore($closingTag); 253 $phpcsFile->fixer->endChangeset(); 254 } 255 } else if ($tokens[$nextContent]['line'] === $tokens[$closingTag]['line']) { 256 $error = 'Closing PHP tag must be on a line by itself'; 257 $fix = $phpcsFile->addFixableError($error, $closingTag, 'ContentAfterEnd'); 258 if ($fix === true) { 259 $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $closingTag, true); 260 $phpcsFile->fixer->beginChangeset(); 261 $phpcsFile->fixer->addNewline($closingTag); 262 $phpcsFile->fixer->addContent($closingTag, str_repeat(' ', ($tokens[$first]['column'] - 1))); 263 $phpcsFile->fixer->endChangeset(); 264 } 265 }//end if 266 267 $next = $phpcsFile->findNext(T_OPEN_TAG, ($closingTag + 1)); 268 if ($next === false) { 269 return; 270 } 271 272 // Check for a blank line at the bottom. 273 if ((isset($tokens[$lastContent]['scope_closer']) === false 274 || $tokens[$lastContent]['scope_closer'] !== $lastContent) 275 && $tokens[$lastContent]['line'] < ($tokens[$closingTag]['line'] - 1) 276 ) { 277 // Find a token on the blank line to throw the error on. 278 $i = $closingTag; 279 do { 280 $i--; 281 } while ($tokens[$i]['line'] !== ($tokens[$closingTag]['line'] - 1)); 282 283 $error = 'Blank line found at end of embedded PHP content'; 284 $fix = $phpcsFile->addFixableError($error, $i, 'SpacingAfter'); 285 if ($fix === true) { 286 $phpcsFile->fixer->beginChangeset(); 287 for ($i = ($lastContent + 1); $i < $closingTag; $i++) { 288 if ($tokens[$i]['line'] === $tokens[$lastContent]['line'] 289 || $tokens[$i]['line'] === $tokens[$closingTag]['line'] 290 ) { 291 continue; 292 } 293 294 $phpcsFile->fixer->replaceToken($i, ''); 295 } 296 297 $phpcsFile->fixer->endChangeset(); 298 } 299 }//end if 300 301 }//end _validateMultilineEmbeddedPhp() 302 303 304 /** 305 * Validates embedded PHP that exists on one line. 306 * 307 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 308 * @param int $stackPtr The position of the current token in the 309 * stack passed in $tokens. 310 * 311 * @return void 312 */ 313 private function _validateInlineEmbeddedPhp(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 314 { 315 $tokens = $phpcsFile->getTokens(); 316 317 // We only want one line PHP sections, so return if the closing tag is 318 // on the next line. 319 $closeTag = $phpcsFile->findNext(T_CLOSE_TAG, $stackPtr, null, false); 320 if ($tokens[$stackPtr]['line'] !== $tokens[$closeTag]['line']) { 321 return; 322 } 323 324 // Check that there is one, and only one space at the start of the statement. 325 $firstContent = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), ($closeTag - 1), true); 326 327 if ($firstContent === false) { 328 $error = 'Empty embedded PHP tag found'; 329 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Empty'); 330 if ($fix === true) { 331 $phpcsFile->fixer->beginChangeset(); 332 for ($i = $stackPtr; $i <= $closeTag; $i++) { 333 $phpcsFile->fixer->replaceToken($i, ''); 334 } 335 336 $phpcsFile->fixer->endChangeset(); 337 } 338 339 return; 340 } 341 342 // The open tag token always contains a single space after it. 343 $leadingSpace = 1; 344 if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) { 345 $leadingSpace = (strlen($tokens[($stackPtr + 1)]['content']) + 1); 346 } 347 348 if ($leadingSpace !== 1) { 349 $error = 'Expected 1 space after opening PHP tag; %s found'; 350 $data = array($leadingSpace); 351 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingAfterOpen', $data); 352 if ($fix === true) { 353 $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); 354 } 355 } 356 357 $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($closeTag - 1), $stackPtr, true); 358 if ((isset($tokens[$prev]['scope_opener']) === false 359 || $tokens[$prev]['scope_opener'] !== $prev) 360 && (isset($tokens[$prev]['scope_closer']) === false 361 || $tokens[$prev]['scope_closer'] !== $prev) 362 && $tokens[$prev]['code'] !== T_SEMICOLON 363 ) { 364 $error = 'Inline PHP statement must end with a semicolon'; 365 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoSemicolon'); 366 if ($fix === true) { 367 $phpcsFile->fixer->addContent($prev, ';'); 368 } 369 } else if ($tokens[$prev]['code'] === T_SEMICOLON) { 370 $statementCount = 1; 371 for ($i = ($stackPtr + 1); $i < $prev; $i++) { 372 if ($tokens[$i]['code'] === T_SEMICOLON) { 373 $statementCount++; 374 } 375 } 376 377 if ($statementCount > 1) { 378 $error = 'Inline PHP statement must contain a single statement; %s found'; 379 $data = array($statementCount); 380 $phpcsFile->addError($error, $stackPtr, 'MultipleStatements', $data); 381 } 382 } 383 384 $trailingSpace = 0; 385 if ($tokens[($closeTag - 1)]['code'] === T_WHITESPACE) { 386 $trailingSpace = strlen($tokens[($closeTag - 1)]['content']); 387 } 388 389 if ($trailingSpace !== 1) { 390 $error = 'Expected 1 space before closing PHP tag; %s found'; 391 $data = array($trailingSpace); 392 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBeforeClose', $data); 393 if ($fix === true) { 394 if ($trailingSpace === 0) { 395 $phpcsFile->fixer->addContentBefore($closeTag, ' '); 396 } else { 397 $phpcsFile->fixer->replaceToken(($closeTag - 1), ' '); 398 } 399 } 400 } 401 402 }//end _validateInlineEmbeddedPhp() 403 404 405}//end class 406