comparison vendor/squizlabs/php_codesniffer/src/Fixer.php @ 17:129ea1e6d783

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