1<?php 2/** 3 * A helper class for fixing errors. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer 9 * @author Greg Sherwood <gsherwood@squiz.net> 10 * @copyright 2006-2012 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 * A helper class for fixing errors. 17 * 18 * Provides helper functions that act upon a token array and modify the file 19 * content. 20 * 21 * @category PHP 22 * @package PHP_CodeSniffer 23 * @author Greg Sherwood <gsherwood@squiz.net> 24 * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) 25 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 26 * @version Release: @package_version@ 27 * @link http://pear.php.net/package/PHP_CodeSniffer 28 */ 29class PHP_CodeSniffer_Fixer 30{ 31 32 /** 33 * Is the fixer enabled and fixing a file? 34 * 35 * Sniffs should check this value to ensure they are not 36 * doing extra processing to prepare for a fix when fixing is 37 * not required. 38 * 39 * @var boolean 40 */ 41 public $enabled = false; 42 43 /** 44 * The number of times we have looped over a file. 45 * 46 * @var int 47 */ 48 public $loops = 0; 49 50 /** 51 * The file being fixed. 52 * 53 * @var PHP_CodeSniffer_File 54 */ 55 private $_currentFile = null; 56 57 /** 58 * The list of tokens that make up the file contents. 59 * 60 * This is a simplified list which just contains the token content and nothing 61 * else. This is the array that is updated as fixes are made, not the file's 62 * token array. Imploding this array will give you the file content back. 63 * 64 * @var array(int => string) 65 */ 66 private $_tokens = array(); 67 68 /** 69 * A list of tokens that have already been fixed. 70 * 71 * We don't allow the same token to be fixed more than once each time 72 * through a file as this can easily cause conflicts between sniffs. 73 * 74 * @var array(int) 75 */ 76 private $_fixedTokens = array(); 77 78 /** 79 * The last value of each fixed token. 80 * 81 * If a token is being "fixed" back to its last value, the fix is 82 * probably conflicting with another. 83 * 84 * @var array(int => string) 85 */ 86 private $_oldTokenValues = array(); 87 88 /** 89 * A list of tokens that have been fixed during a changeset. 90 * 91 * All changes in changeset must be able to be applied, or else 92 * the entire changeset is rejected. 93 * 94 * @var array() 95 */ 96 private $_changeset = array(); 97 98 /** 99 * Is there an open changeset. 100 * 101 * @var boolean 102 */ 103 private $_inChangeset = false; 104 105 /** 106 * Is the current fixing loop in conflict? 107 * 108 * @var boolean 109 */ 110 private $_inConflict = false; 111 112 /** 113 * The number of fixes that have been performed. 114 * 115 * @var int 116 */ 117 private $_numFixes = 0; 118 119 120 /** 121 * Starts fixing a new file. 122 * 123 * @param PHP_CodeSniffer_File $phpcsFile The file being fixed. 124 * 125 * @return void 126 */ 127 public function startFile($phpcsFile) 128 { 129 $this->_currentFile = $phpcsFile; 130 $this->_numFixes = 0; 131 $this->_fixedTokens = array(); 132 133 $tokens = $phpcsFile->getTokens(); 134 $this->_tokens = array(); 135 foreach ($tokens as $index => $token) { 136 if (isset($token['orig_content']) === true) { 137 $this->_tokens[$index] = $token['orig_content']; 138 } else { 139 $this->_tokens[$index] = $token['content']; 140 } 141 } 142 143 }//end startFile() 144 145 146 /** 147 * Attempt to fix the file by processing it until no fixes are made. 148 * 149 * @return boolean 150 */ 151 public function fixFile() 152 { 153 $fixable = $this->_currentFile->getFixableCount(); 154 if ($fixable === 0) { 155 // Nothing to fix. 156 return false; 157 } 158 159 $stdin = false; 160 $cliValues = $this->_currentFile->phpcs->cli->getCommandLineValues(); 161 if (empty($cliValues['files']) === true) { 162 $stdin = true; 163 } 164 165 $this->enabled = true; 166 167 $this->loops = 0; 168 while ($this->loops < 50) { 169 ob_start(); 170 171 // Only needed once file content has changed. 172 $contents = $this->getContents(); 173 174 if (PHP_CODESNIFFER_VERBOSITY > 2) { 175 @ob_end_clean(); 176 echo '---START FILE CONTENT---'.PHP_EOL; 177 $lines = explode($this->_currentFile->eolChar, $contents); 178 $max = strlen(count($lines)); 179 foreach ($lines as $lineNum => $line) { 180 $lineNum++; 181 echo str_pad($lineNum, $max, ' ', STR_PAD_LEFT).'|'.$line.PHP_EOL; 182 } 183 184 echo '--- END FILE CONTENT ---'.PHP_EOL; 185 ob_start(); 186 } 187 188 $this->_inConflict = false; 189 $this->_currentFile->refreshTokenListeners(); 190 $this->_currentFile->start($contents); 191 ob_end_clean(); 192 193 $this->loops++; 194 195 if (PHP_CODESNIFFER_CBF === true && $stdin === false) { 196 echo "\r".str_repeat(' ', 80)."\r"; 197 echo "\t=> Fixing file: $this->_numFixes/$fixable violations remaining [made $this->loops pass"; 198 if ($this->loops > 1) { 199 echo 'es'; 200 } 201 202 echo ']... '; 203 } 204 205 if ($this->_numFixes === 0 && $this->_inConflict === false) { 206 // Nothing left to do. 207 break; 208 } else if (PHP_CODESNIFFER_VERBOSITY > 1) { 209 echo "\t* fixed $this->_numFixes violations, starting loop ".($this->loops + 1).' *'.PHP_EOL; 210 } 211 }//end while 212 213 $this->enabled = false; 214 215 if ($this->_numFixes > 0) { 216 if (PHP_CODESNIFFER_VERBOSITY > 1) { 217 @ob_end_clean(); 218 echo "\t*** Reached maximum number of loops with $this->_numFixes violations left unfixed ***".PHP_EOL; 219 ob_start(); 220 } 221 222 return false; 223 } 224 225 return true; 226 227 }//end fixFile() 228 229 230 /** 231 * Generates a text diff of the original file and the new content. 232 * 233 * @param string $filePath Optional file path to diff the file against. 234 * If not specified, the original version of the 235 * file will be used. 236 * @param boolean $colors Print colored output or not. 237 * 238 * @return string 239 */ 240 public function generateDiff($filePath=null, $colors=true) 241 { 242 if ($filePath === null) { 243 $filePath = $this->_currentFile->getFilename(); 244 } 245 246 $cwd = getcwd().DIRECTORY_SEPARATOR; 247 if (strpos($filePath, $cwd) === 0) { 248 $filename = substr($filePath, strlen($cwd)); 249 } else { 250 $filename = $filePath; 251 } 252 253 $contents = $this->getContents(); 254 255 if (function_exists('sys_get_temp_dir') === true) { 256 // This is needed for HHVM support, but only available from 5.2.1. 257 $tempName = tempnam(sys_get_temp_dir(), 'phpcs-fixer'); 258 $fixedFile = fopen($tempName, 'w'); 259 } else { 260 $fixedFile = tmpfile(); 261 $data = stream_get_meta_data($fixedFile); 262 $tempName = $data['uri']; 263 } 264 265 fwrite($fixedFile, $contents); 266 267 // We must use something like shell_exec() because whitespace at the end 268 // of lines is critical to diff files. 269 $filename = escapeshellarg($filename); 270 $cmd = "diff -u -L$filename -LPHP_CodeSniffer $filename \"$tempName\""; 271 272 $diff = shell_exec($cmd); 273 274 fclose($fixedFile); 275 if (is_file($tempName) === true) { 276 unlink($tempName); 277 } 278 279 if ($colors === false) { 280 return $diff; 281 } 282 283 $diffLines = explode(PHP_EOL, $diff); 284 if (count($diffLines) === 1) { 285 // Seems to be required for cygwin. 286 $diffLines = explode("\n", $diff); 287 } 288 289 $diff = array(); 290 foreach ($diffLines as $line) { 291 if (isset($line[0]) === true) { 292 switch ($line[0]) { 293 case '-': 294 $diff[] = "\033[31m$line\033[0m"; 295 break; 296 case '+': 297 $diff[] = "\033[32m$line\033[0m"; 298 break; 299 default: 300 $diff[] = $line; 301 } 302 } 303 } 304 305 $diff = implode(PHP_EOL, $diff); 306 307 return $diff; 308 309 }//end generateDiff() 310 311 312 /** 313 * Get a count of fixes that have been performed on the file. 314 * 315 * This value is reset every time a new file is started, or an existing 316 * file is restarted. 317 * 318 * @return int 319 */ 320 public function getFixCount() 321 { 322 return $this->_numFixes; 323 324 }//end getFixCount() 325 326 327 /** 328 * Get the current content of the file, as a string. 329 * 330 * @return string 331 */ 332 public function getContents() 333 { 334 $contents = implode($this->_tokens); 335 return $contents; 336 337 }//end getContents() 338 339 340 /** 341 * Get the current fixed content of a token. 342 * 343 * This function takes changesets into account so should be used 344 * instead of directly accessing the token array. 345 * 346 * @param int $stackPtr The position of the token in the token stack. 347 * 348 * @return string 349 */ 350 public function getTokenContent($stackPtr) 351 { 352 if ($this->_inChangeset === true 353 && isset($this->_changeset[$stackPtr]) === true 354 ) { 355 return $this->_changeset[$stackPtr]; 356 } else { 357 return $this->_tokens[$stackPtr]; 358 } 359 360 }//end getTokenContent() 361 362 363 /** 364 * Start recording actions for a changeset. 365 * 366 * @return void 367 */ 368 public function beginChangeset() 369 { 370 if ($this->_inConflict === true) { 371 return false; 372 } 373 374 if (PHP_CODESNIFFER_VERBOSITY > 1) { 375 $bt = debug_backtrace(); 376 $sniff = $bt[1]['class']; 377 $line = $bt[0]['line']; 378 379 @ob_end_clean(); 380 echo "\t=> Changeset started by $sniff (line $line)".PHP_EOL; 381 ob_start(); 382 } 383 384 $this->_changeset = array(); 385 $this->_inChangeset = true; 386 387 }//end beginChangeset() 388 389 390 /** 391 * Stop recording actions for a changeset, and apply logged changes. 392 * 393 * @return boolean 394 */ 395 public function endChangeset() 396 { 397 if ($this->_inConflict === true) { 398 return false; 399 } 400 401 $this->_inChangeset = false; 402 403 $success = true; 404 $applied = array(); 405 foreach ($this->_changeset as $stackPtr => $content) { 406 $success = $this->replaceToken($stackPtr, $content); 407 if ($success === false) { 408 break; 409 } else { 410 $applied[] = $stackPtr; 411 } 412 } 413 414 if ($success === false) { 415 // Rolling back all changes. 416 foreach ($applied as $stackPtr) { 417 $this->revertToken($stackPtr); 418 } 419 420 if (PHP_CODESNIFFER_VERBOSITY > 1) { 421 @ob_end_clean(); 422 echo "\t=> Changeset failed to apply".PHP_EOL; 423 ob_start(); 424 } 425 } else if (PHP_CODESNIFFER_VERBOSITY > 1) { 426 $fixes = count($this->_changeset); 427 @ob_end_clean(); 428 echo "\t=> Changeset ended: $fixes changes applied".PHP_EOL; 429 ob_start(); 430 } 431 432 $this->_changeset = array(); 433 434 }//end endChangeset() 435 436 437 /** 438 * Stop recording actions for a changeset, and discard logged changes. 439 * 440 * @return void 441 */ 442 public function rollbackChangeset() 443 { 444 $this->_inChangeset = false; 445 $this->_inConflict = false; 446 447 if (empty($this->_changeset) === false) { 448 if (PHP_CODESNIFFER_VERBOSITY > 1) { 449 $bt = debug_backtrace(); 450 if ($bt[1]['class'] === 'PHP_CodeSniffer_Fixer') { 451 $sniff = $bt[2]['class']; 452 $line = $bt[1]['line']; 453 } else { 454 $sniff = $bt[1]['class']; 455 $line = $bt[0]['line']; 456 } 457 458 $numChanges = count($this->_changeset); 459 460 @ob_end_clean(); 461 echo "\t\tR: $sniff (line $line) rolled back the changeset ($numChanges changes)".PHP_EOL; 462 echo "\t=> Changeset rolled back".PHP_EOL; 463 ob_start(); 464 } 465 466 $this->_changeset = array(); 467 }//end if 468 469 }//end rollbackChangeset() 470 471 472 /** 473 * Replace the entire contents of a token. 474 * 475 * @param int $stackPtr The position of the token in the token stack. 476 * @param string $content The new content of the token. 477 * 478 * @return bool If the change was accepted. 479 */ 480 public function replaceToken($stackPtr, $content) 481 { 482 if ($this->_inConflict === true) { 483 return false; 484 } 485 486 if ($this->_inChangeset === false 487 && isset($this->_fixedTokens[$stackPtr]) === true 488 ) { 489 $indent = "\t"; 490 if (empty($this->_changeset) === false) { 491 $indent .= "\t"; 492 } 493 494 if (PHP_CODESNIFFER_VERBOSITY > 1) { 495 @ob_end_clean(); 496 echo "$indent* token $stackPtr has already been modified, skipping *".PHP_EOL; 497 ob_start(); 498 } 499 500 return false; 501 } 502 503 if (PHP_CODESNIFFER_VERBOSITY > 1) { 504 $bt = debug_backtrace(); 505 if ($bt[1]['class'] === 'PHP_CodeSniffer_Fixer') { 506 $sniff = $bt[2]['class']; 507 $line = $bt[1]['line']; 508 } else { 509 $sniff = $bt[1]['class']; 510 $line = $bt[0]['line']; 511 } 512 513 $tokens = $this->_currentFile->getTokens(); 514 $type = $tokens[$stackPtr]['type']; 515 $oldContent = PHP_CodeSniffer::prepareForOutput($this->_tokens[$stackPtr]); 516 $newContent = PHP_CodeSniffer::prepareForOutput($content); 517 if (trim($this->_tokens[$stackPtr]) === '' && isset($this->_tokens[($stackPtr + 1)]) === true) { 518 // Add some context for whitespace only changes. 519 $append = PHP_CodeSniffer::prepareForOutput($this->_tokens[($stackPtr + 1)]); 520 $oldContent .= $append; 521 $newContent .= $append; 522 } 523 }//end if 524 525 if ($this->_inChangeset === true) { 526 $this->_changeset[$stackPtr] = $content; 527 528 if (PHP_CODESNIFFER_VERBOSITY > 1) { 529 @ob_end_clean(); 530 echo "\t\tQ: $sniff (line $line) replaced token $stackPtr ($type) \"$oldContent\" => \"$newContent\"".PHP_EOL; 531 ob_start(); 532 } 533 534 return true; 535 } 536 537 if (isset($this->_oldTokenValues[$stackPtr]) === false) { 538 $this->_oldTokenValues[$stackPtr] = array( 539 'curr' => $content, 540 'prev' => $this->_tokens[$stackPtr], 541 'loop' => $this->loops, 542 ); 543 } else { 544 if ($this->_oldTokenValues[$stackPtr]['prev'] === $content 545 && $this->_oldTokenValues[$stackPtr]['loop'] === ($this->loops - 1) 546 ) { 547 if (PHP_CODESNIFFER_VERBOSITY > 1) { 548 $indent = "\t"; 549 if (empty($this->_changeset) === false) { 550 $indent .= "\t"; 551 } 552 553 $loop = $this->_oldTokenValues[$stackPtr]['loop']; 554 555 @ob_end_clean(); 556 echo "$indent**** $sniff (line $line) has possible conflict with another sniff on loop $loop; caused by the following change ****".PHP_EOL; 557 echo "$indent**** replaced token $stackPtr ($type) \"$oldContent\" => \"$newContent\" ****".PHP_EOL; 558 } 559 560 if ($this->_oldTokenValues[$stackPtr]['loop'] >= ($this->loops - 1)) { 561 $this->_inConflict = true; 562 if (PHP_CODESNIFFER_VERBOSITY > 1) { 563 echo "$indent**** ignoring all changes until next loop ****".PHP_EOL; 564 } 565 } 566 567 if (PHP_CODESNIFFER_VERBOSITY > 1) { 568 ob_start(); 569 } 570 571 return false; 572 }//end if 573 574 $this->_oldTokenValues[$stackPtr]['prev'] = $this->_oldTokenValues[$stackPtr]['curr']; 575 $this->_oldTokenValues[$stackPtr]['curr'] = $content; 576 $this->_oldTokenValues[$stackPtr]['loop'] = $this->loops; 577 }//end if 578 579 $this->_fixedTokens[$stackPtr] = $this->_tokens[$stackPtr]; 580 $this->_tokens[$stackPtr] = $content; 581 $this->_numFixes++; 582 583 if (PHP_CODESNIFFER_VERBOSITY > 1) { 584 $indent = "\t"; 585 if (empty($this->_changeset) === false) { 586 $indent .= "\tA: "; 587 } 588 589 @ob_end_clean(); 590 echo "$indent$sniff (line $line) replaced token $stackPtr ($type) \"$oldContent\" => \"$newContent\"".PHP_EOL; 591 ob_start(); 592 } 593 594 return true; 595 596 }//end replaceToken() 597 598 599 /** 600 * Reverts the previous fix made to a token. 601 * 602 * @param int $stackPtr The position of the token in the token stack. 603 * 604 * @return bool If a change was reverted. 605 */ 606 public function revertToken($stackPtr) 607 { 608 if (isset($this->_fixedTokens[$stackPtr]) === false) { 609 return false; 610 } 611 612 if (PHP_CODESNIFFER_VERBOSITY > 1) { 613 $bt = debug_backtrace(); 614 if ($bt[1]['class'] === 'PHP_CodeSniffer_Fixer') { 615 $sniff = $bt[2]['class']; 616 $line = $bt[1]['line']; 617 } else { 618 $sniff = $bt[1]['class']; 619 $line = $bt[0]['line']; 620 } 621 622 $tokens = $this->_currentFile->getTokens(); 623 $type = $tokens[$stackPtr]['type']; 624 $oldContent = PHP_CodeSniffer::prepareForOutput($this->_tokens[$stackPtr]); 625 $newContent = PHP_CodeSniffer::prepareForOutput($this->_fixedTokens[$stackPtr]); 626 if (trim($this->_tokens[$stackPtr]) === '' && isset($tokens[($stackPtr + 1)]) === true) { 627 // Add some context for whitespace only changes. 628 $append = PHP_CodeSniffer::prepareForOutput($this->_tokens[($stackPtr + 1)]); 629 $oldContent .= $append; 630 $newContent .= $append; 631 } 632 }//end if 633 634 $this->_tokens[$stackPtr] = $this->_fixedTokens[$stackPtr]; 635 unset($this->_fixedTokens[$stackPtr]); 636 $this->_numFixes--; 637 638 if (PHP_CODESNIFFER_VERBOSITY > 1) { 639 $indent = "\t"; 640 if (empty($this->_changeset) === false) { 641 $indent .= "\tR: "; 642 } 643 644 @ob_end_clean(); 645 echo "$indent$sniff (line $line) reverted token $stackPtr ($type) \"$oldContent\" => \"$newContent\"".PHP_EOL; 646 ob_start(); 647 } 648 649 return true; 650 651 }//end revertToken() 652 653 654 /** 655 * Replace the content of a token with a part of its current content. 656 * 657 * @param int $stackPtr The position of the token in the token stack. 658 * @param int $start The first character to keep. 659 * @param int $length The number of chacters to keep. If NULL, the content of 660 * the token from $start to the end of the content is kept. 661 * 662 * @return bool If the change was accepted. 663 */ 664 public function substrToken($stackPtr, $start, $length=null) 665 { 666 $current = $this->getTokenContent($stackPtr); 667 668 if ($length === null) { 669 $newContent = substr($current, $start); 670 } else { 671 $newContent = substr($current, $start, $length); 672 } 673 674 return $this->replaceToken($stackPtr, $newContent); 675 676 }//end substrToken() 677 678 679 /** 680 * Adds a newline to end of a token's content. 681 * 682 * @param int $stackPtr The position of the token in the token stack. 683 * 684 * @return bool If the change was accepted. 685 */ 686 public function addNewline($stackPtr) 687 { 688 $current = $this->getTokenContent($stackPtr); 689 return $this->replaceToken($stackPtr, $current.$this->_currentFile->eolChar); 690 691 }//end addNewline() 692 693 694 /** 695 * Adds a newline to the start of a token's content. 696 * 697 * @param int $stackPtr The position of the token in the token stack. 698 * 699 * @return bool If the change was accepted. 700 */ 701 public function addNewlineBefore($stackPtr) 702 { 703 $current = $this->getTokenContent($stackPtr); 704 return $this->replaceToken($stackPtr, $this->_currentFile->eolChar.$current); 705 706 }//end addNewlineBefore() 707 708 709 /** 710 * Adds content to the end of a token's current content. 711 * 712 * @param int $stackPtr The position of the token in the token stack. 713 * @param string $content The content to add. 714 * 715 * @return bool If the change was accepted. 716 */ 717 public function addContent($stackPtr, $content) 718 { 719 $current = $this->getTokenContent($stackPtr); 720 return $this->replaceToken($stackPtr, $current.$content); 721 722 }//end addContent() 723 724 725 /** 726 * Adds content to the start of a token's current content. 727 * 728 * @param int $stackPtr The position of the token in the token stack. 729 * @param string $content The content to add. 730 * 731 * @return bool If the change was accepted. 732 */ 733 public function addContentBefore($stackPtr, $content) 734 { 735 $current = $this->getTokenContent($stackPtr); 736 return $this->replaceToken($stackPtr, $content.$current); 737 738 }//end addContentBefore() 739 740 741}//end class 742