@@ -35,6 +35,27 @@ class TabAndSpaceSniff implements Sniff {
3535 'CSS ' ,
3636 ];
3737
38+ /**
39+ * Maximum number of errors to fix in a single file to prevent timeout.
40+ * This prevents "FAILED TO FIX" on very large files.
41+ *
42+ * @var int
43+ */
44+ protected static int $ fixCount = 0 ;
45+
46+ /**
47+ * @var int
48+ */
49+ protected const MAX_FIXES_PER_FILE = 100 ;
50+
51+ /**
52+ * Track fixed positions to detect infinite loops.
53+ * Maps "filename:stackPtr" to attempt count.
54+ *
55+ * @var array<string, int>
56+ */
57+ protected static array $ fixedPositions = [];
58+
3859 /**
3960 * @inheritDoc
4061 */
@@ -46,6 +67,13 @@ public function register(): array {
4667 * @inheritDoc
4768 */
4869 public function process (File $ phpcsFile , int $ stackPtr ): void {
70+ // Reset counters at start of new file
71+ static $ lastFile = null ;
72+ if ($ lastFile !== $ phpcsFile ->getFilename ()) {
73+ static ::$ fixCount = 0 ;
74+ static ::$ fixedPositions = [];
75+ $ lastFile = $ phpcsFile ->getFilename ();
76+ }
4977 $ tokens = $ phpcsFile ->getTokens ();
5078
5179 $ line = $ tokens [$ stackPtr ]['line ' ];
@@ -56,23 +84,58 @@ public function process(File $phpcsFile, int $stackPtr): void {
5684
5785 $ content = $ tokens [$ stackPtr ]['orig_content ' ] ?? $ tokens [$ stackPtr ]['content ' ];
5886
87+ // Detect infinite loops: if we've tried to fix this position more than twice, skip it
88+ $ posKey = $ phpcsFile ->getFilename () . ': ' . $ stackPtr ;
89+ if (isset (static ::$ fixedPositions [$ posKey ]) && static ::$ fixedPositions [$ posKey ] > 2 ) {
90+ // Infinite loop detected - report as non-fixable
91+ $ error = 'Mixed tabs and spaces in indentation; use tabs only (cannot auto-fix due to conflict) ' ;
92+ $ phpcsFile ->addError ($ error , $ stackPtr , 'MixedIndentationConflict ' );
93+
94+ return ;
95+ }
96+
5997 // Check for space followed by tab (wrong order)
6098 if (str_contains ($ content , ' ' . "\t" )) {
6199 $ error = 'Space followed by tab found in indentation; use tabs only ' ;
62- $ fix = $ phpcsFile ->addFixableError ($ error , $ stackPtr , 'SpaceBeforeTab ' );
63- if ($ fix ) {
64- // Replace space+tab patterns with just tabs
65- $ phpcsFile ->fixer ->replaceToken ($ stackPtr , str_replace (" \t" , "\t" , $ content ));
100+ // Only mark as fixable if we haven't exceeded the limit
101+ if (static ::$ fixCount < static ::MAX_FIXES_PER_FILE ) {
102+ $ fix = $ phpcsFile ->addFixableError ($ error , $ stackPtr , 'SpaceBeforeTab ' );
103+ if ($ fix ) {
104+ static ::$ fixCount ++;
105+ // Track this fix attempt to detect infinite loops
106+ static ::$ fixedPositions [$ posKey ] = (static ::$ fixedPositions [$ posKey ] ?? 0 ) + 1 ;
107+ // Replace space+tab patterns with just tabs
108+ $ fixed = str_replace (" \t" , "\t" , $ content );
109+ if ($ fixed !== $ content ) {
110+ $ phpcsFile ->fixer ->replaceToken ($ stackPtr , $ fixed );
111+ }
112+ }
113+ } else {
114+ // Report as non-fixable error to avoid timeout
115+ $ phpcsFile ->addError ($ error . ' (auto-fix limit reached) ' , $ stackPtr , 'SpaceBeforeTabLimitReached ' );
66116 }
67117 }
68118
69119 // Check for tab followed by space (mixed indentation)
70120 if (str_contains ($ content , "\t " )) {
71121 $ error = 'Tab followed by space found in indentation; use tabs only ' ;
72- $ fix = $ phpcsFile ->addFixableError ($ error , $ stackPtr , 'TabAndSpace ' );
73- if ($ fix ) {
74- // Remove spaces after tabs at start of line
75- $ phpcsFile ->fixer ->replaceToken ($ stackPtr , preg_replace ('/^(\t+) +/ ' , '$1 ' , $ content ));
122+ // Only mark as fixable if we haven't exceeded the limit
123+ if (static ::$ fixCount < static ::MAX_FIXES_PER_FILE ) {
124+ $ fix = $ phpcsFile ->addFixableError ($ error , $ stackPtr , 'TabAndSpace ' );
125+ if ($ fix ) {
126+ static ::$ fixCount ++;
127+ // Track this fix attempt to detect infinite loops
128+ static ::$ fixedPositions [$ posKey ] = (static ::$ fixedPositions [$ posKey ] ?? 0 ) + 1 ;
129+ // Remove spaces after tabs at start of line
130+ $ fixed = preg_replace ('/^(\t+) +/ ' , '$1 ' , $ content );
131+ // Only apply fix if content actually changed
132+ if ($ fixed !== null && $ fixed !== $ content ) {
133+ $ phpcsFile ->fixer ->replaceToken ($ stackPtr , $ fixed );
134+ }
135+ }
136+ } else {
137+ // Report as non-fixable error to avoid timeout
138+ $ phpcsFile ->addError ($ error . ' (auto-fix limit reached) ' , $ stackPtr , 'TabAndSpaceLimitReached ' );
76139 }
77140 }
78141 }
0 commit comments