@@ -71,11 +71,17 @@ public function process(File $phpcsFile, int $stackPtr): void {
7171 // Get the expected indentation based on scope
7272 $ expectedIndent = $ this ->getExpectedIndent ($ phpcsFile , $ nextToken , $ tokens );
7373
74- // Allow continuation lines (lines can be indented more for alignment)
75- // But check if the previous line suggests this should NOT be indented more
74+ // Check if line is over-indented (more than expected for its scope)
7675 if ($ currentIndent > $ expectedIndent ) {
76+ // Check if this line starts with a continuation operator
77+ if ($ this ->startsWithContinuationOperator ($ nextToken , $ tokens )) {
78+ return ; // Valid continuation line
79+ }
80+
7781 $ prevLine = $ this ->findPreviousContentLine ($ phpcsFile , $ stackPtr , $ tokens );
78- if ($ prevLine !== null && $ this ->isOrphanedIndent ($ phpcsFile , $ prevLine , $ currentIndent , $ expectedIndent , $ tokens )) {
82+
83+ // Check if this is a valid continuation line or incorrectly indented
84+ if ($ prevLine !== null && !$ this ->isValidContinuation ($ phpcsFile , $ prevLine , $ tokens )) {
7985 $ error = 'Line indented incorrectly; expected %d tabs, found %d ' ;
8086 $ data = [$ expectedIndent , $ currentIndent ];
8187 $ fix = $ phpcsFile ->addFixableError ($ error , $ stackPtr , 'Incorrect ' , $ data );
@@ -146,48 +152,82 @@ protected function findPreviousContentLine(File $phpcsFile, int $stackPtr, array
146152 }
147153
148154 /**
149- * Check if this looks like orphaned indentation (not a valid continuation).
155+ * Check if this line starts with a continuation operator.
156+ *
157+ * @param int $nextToken First non-whitespace token on the line
158+ * @param array $tokens
159+ *
160+ * @return bool
161+ */
162+ protected function startsWithContinuationOperator (int $ nextToken , array $ tokens ): bool {
163+ $ continuationStarters = [
164+ \T_STRING_CONCAT ,
165+ \T_OBJECT_OPERATOR ,
166+ \T_NULLSAFE_OBJECT_OPERATOR ,
167+ \T_BOOLEAN_AND ,
168+ \T_BOOLEAN_OR ,
169+ \T_LOGICAL_AND ,
170+ \T_LOGICAL_OR ,
171+ \T_PLUS ,
172+ \T_MINUS ,
173+ \T_MULTIPLY ,
174+ \T_DIVIDE ,
175+ ];
176+
177+ return in_array ($ tokens [$ nextToken ]['code ' ], $ continuationStarters , true );
178+ }
179+
180+ /**
181+ * Check if this looks like a valid continuation line (allowed to have extra indentation).
150182 *
151183 * @param \PHP_CodeSniffer\Files\File $phpcsFile
152184 * @param int $prevToken Previous content token
153- * @param int $currentIndent
154- * @param int $expectedIndent
155185 * @param array $tokens
156186 *
157- * @return bool
187+ * @return bool True if this is a valid continuation, false if it should match scope indent
158188 */
159- protected function isOrphanedIndent (File $ phpcsFile , int $ prevToken , int $ currentIndent , int $ expectedIndent , array $ tokens ): bool {
160- // Pattern 1: Previous line was a closing brace
161- // This catches: } \n orphaned code
162- if ($ tokens [$ prevToken ]['code ' ] === T_CLOSE_CURLY_BRACKET ) {
189+ protected function isValidContinuation (File $ phpcsFile , int $ prevToken , array $ tokens ): bool {
190+ $ prevCode = $ tokens [$ prevToken ]['code ' ];
191+
192+ // Tokens that indicate the next line is a continuation
193+ $ continuationTokens = [
194+ \T_PLUS ,
195+ \T_MINUS ,
196+ \T_MULTIPLY ,
197+ \T_DIVIDE ,
198+ \T_MODULUS ,
199+ \T_STRING_CONCAT ,
200+ \T_COMMA ,
201+ \T_OPEN_PARENTHESIS ,
202+ \T_OPEN_SQUARE_BRACKET ,
203+ \T_OPEN_SHORT_ARRAY ,
204+ \T_DOUBLE_ARROW ,
205+ \T_BOOLEAN_AND ,
206+ \T_BOOLEAN_OR ,
207+ \T_LOGICAL_AND ,
208+ \T_LOGICAL_OR ,
209+ \T_INSTANCEOF ,
210+ \T_INLINE_THEN ,
211+ \T_COALESCE ,
212+ \T_OBJECT_OPERATOR ,
213+ \T_NULLSAFE_OBJECT_OPERATOR ,
214+ \T_EQUAL ,
215+ \T_PLUS_EQUAL ,
216+ \T_MINUS_EQUAL ,
217+ \T_MUL_EQUAL ,
218+ \T_DIV_EQUAL ,
219+ \T_CONCAT_EQUAL ,
220+ \T_MOD_EQUAL ,
221+ ];
222+
223+ if (in_array ($ prevCode , $ continuationTokens , true )) {
163224 return true ;
164225 }
165226
166- // Pattern 2: Previous line ended with }; (closure, array, etc.)
167- // This catches: }; \n orphaned code
168- if ($ tokens [$ prevToken ]['code ' ] === T_SEMICOLON ) {
169- // Check if the token before semicolon is a closing brace
170- $ beforeSemicolon = $ phpcsFile ->findPrevious (T_WHITESPACE , $ prevToken - 1 , null , true );
171- if ($ beforeSemicolon !== false && $ tokens [$ beforeSemicolon ]['code ' ] === T_CLOSE_CURLY_BRACKET ) {
172- return true ;
173- }
174-
175- // Pattern 3: Previous line is at the same over-indented level
176- // This catches consecutive orphaned lines: } \n line1; \n line2;
177- // Find the start of the previous line to check its indentation
178- $ prevLineStart = $ prevToken ;
179- while ($ prevLineStart > 0 && $ tokens [$ prevLineStart - 1 ]['line ' ] === $ tokens [$ prevToken ]['line ' ]) {
180- $ prevLineStart --;
181- }
182-
183- // Check if previous line started with whitespace
184- if ($ tokens [$ prevLineStart ]['code ' ] === T_WHITESPACE ) {
185- $ prevIndent = $ this ->getIndentLevel ($ tokens [$ prevLineStart ]);
186- // If previous line was also over-indented at the same level, this is likely orphaned too
187- if ($ prevIndent === $ currentIndent && $ prevIndent > $ expectedIndent ) {
188- return true ;
189- }
190- }
227+ // Check string representation for bracket tokens (PHPCS sometimes uses string codes)
228+ $ prevContent = $ tokens [$ prevToken ]['content ' ] ?? '' ;
229+ if ($ prevContent === '[ ' || $ prevContent === '( ' ) {
230+ return true ;
191231 }
192232
193233 return false ;
0 commit comments