Skip to content

Commit bfff5aa

Browse files
authored
Fix phpcbf conflict on anonymous class bodies in ConsistentIndent (#68)
PhpCollective.WhiteSpace.ConsistentIndent and the bundled Generic.WhiteSpace.ScopeIndent disagreed on the expected indent of an anonymous class body when the class is passed as an argument to a multi-line function call, e.g.: return new Service( new class ($payload) extends Base { public function foo(): void {} }, ); The class scope opens inside an unclosed parenthesis, so the body carries one continuation indent level. getExpectedIndent() only counts scope conditions and misses it, so ConsistentIndent dedented the body while ScopeIndent re-indented it. phpcbf then oscillated until it hit the loop limit and reported "FAILED TO FIX" (exit 7), leaving the file unfixable. Defer to ScopeIndent for anonymous class bodies, the same way closures and arrays are already skipped, by adding an isInsideAnonClass() guard.
1 parent f4a80d6 commit bfff5aa

3 files changed

Lines changed: 69 additions & 0 deletions

File tree

PhpCollective/Sniffs/WhiteSpace/ConsistentIndentSniff.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ public function process(File $phpcsFile, $stackPtr): void
7777
if ($this->isInsideSwitchCase($phpcsFile, $nextToken, $tokens)) {
7878
return;
7979
}
80+
if ($this->isInsideAnonClass($tokens, $nextToken)) {
81+
return;
82+
}
8083
if ($tokens[$nextToken]['code'] === T_COMMENT || $tokens[$nextToken]['code'] === T_DOC_COMMENT_OPEN_TAG) {
8184
return;
8285
}
@@ -482,6 +485,38 @@ protected function isInsideSwitchCase(File $phpcsFile, int $stackPtr, array $tok
482485
return false;
483486
}
484487

488+
/**
489+
* Check if the current position is inside an anonymous class body.
490+
*
491+
* Anonymous classes are frequently passed as arguments to a multi-line
492+
* function call (e.g. `new Service(new class () extends Base { ... })`).
493+
* In that case the class scope is opened inside an unclosed parenthesis,
494+
* so the body carries one continuation indent level that `getExpectedIndent()`
495+
* (which only counts scope conditions) cannot see. `Generic.WhiteSpace.ScopeIndent`
496+
* does account for it, so flagging here produces a fixer conflict where the two
497+
* sniffs dedent/indent the same lines forever ("FAILED TO FIX"). Defer to
498+
* ScopeIndent for anonymous class bodies, the same way closures are skipped.
499+
*
500+
* @param array<int, array<string, mixed>> $tokens
501+
* @param int $stackPtr
502+
*
503+
* @return bool
504+
*/
505+
protected function isInsideAnonClass(array $tokens, int $stackPtr): bool
506+
{
507+
if (empty($tokens[$stackPtr]['conditions'])) {
508+
return false;
509+
}
510+
511+
foreach ($tokens[$stackPtr]['conditions'] as $code) {
512+
if ($code === T_ANON_CLASS) {
513+
return true;
514+
}
515+
}
516+
517+
return false;
518+
}
519+
485520
/**
486521
* Check if the previous line is "complete" (ends with statement terminator or closing brace).
487522
* If not, the next line might be a continuation.

tests/_data/ConsistentIndent/after.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,21 @@ public function nullCoalesceShouldNotBeFlagged($params): ?int
108108
?? $params['home_id']
109109
?? null;
110110
}
111+
112+
public function anonClassAsArgumentShouldNotBeFlagged(array $payload): object
113+
{
114+
return new Service(
115+
new class ($payload) extends Base {
116+
public function __construct(private array $payload)
117+
{
118+
parent::__construct();
119+
}
120+
121+
public function defaultProvider(): string
122+
{
123+
return 'codex';
124+
}
125+
},
126+
);
127+
}
111128
}

tests/_data/ConsistentIndent/before.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,21 @@ public function nullCoalesceShouldNotBeFlagged($params): ?int
108108
?? $params['home_id']
109109
?? null;
110110
}
111+
112+
public function anonClassAsArgumentShouldNotBeFlagged(array $payload): object
113+
{
114+
return new Service(
115+
new class ($payload) extends Base {
116+
public function __construct(private array $payload)
117+
{
118+
parent::__construct();
119+
}
120+
121+
public function defaultProvider(): string
122+
{
123+
return 'codex';
124+
}
125+
},
126+
);
127+
}
111128
}

0 commit comments

Comments
 (0)