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