From ebf0c3108598e537ab7e1f35b3de916ed6e55a04 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 28 May 2026 17:34:41 +0200 Subject: [PATCH] Fix phpcbf conflict on anonymous class bodies in ConsistentIndent 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. --- .../WhiteSpace/ConsistentIndentSniff.php | 35 +++++++++++++++++++ tests/_data/ConsistentIndent/after.php | 17 +++++++++ tests/_data/ConsistentIndent/before.php | 17 +++++++++ 3 files changed, 69 insertions(+) diff --git a/PhpCollective/Sniffs/WhiteSpace/ConsistentIndentSniff.php b/PhpCollective/Sniffs/WhiteSpace/ConsistentIndentSniff.php index 2f6446d..ae230e7 100644 --- a/PhpCollective/Sniffs/WhiteSpace/ConsistentIndentSniff.php +++ b/PhpCollective/Sniffs/WhiteSpace/ConsistentIndentSniff.php @@ -77,6 +77,9 @@ public function process(File $phpcsFile, $stackPtr): void if ($this->isInsideSwitchCase($phpcsFile, $nextToken, $tokens)) { return; } + if ($this->isInsideAnonClass($tokens, $nextToken)) { + return; + } if ($tokens[$nextToken]['code'] === T_COMMENT || $tokens[$nextToken]['code'] === T_DOC_COMMENT_OPEN_TAG) { return; } @@ -482,6 +485,38 @@ protected function isInsideSwitchCase(File $phpcsFile, int $stackPtr, array $tok return false; } + /** + * Check if the current position is inside an anonymous class body. + * + * Anonymous classes are frequently passed as arguments to a multi-line + * function call (e.g. `new Service(new class () extends Base { ... })`). + * In that case the class scope is opened inside an unclosed parenthesis, + * so the body carries one continuation indent level that `getExpectedIndent()` + * (which only counts scope conditions) cannot see. `Generic.WhiteSpace.ScopeIndent` + * does account for it, so flagging here produces a fixer conflict where the two + * sniffs dedent/indent the same lines forever ("FAILED TO FIX"). Defer to + * ScopeIndent for anonymous class bodies, the same way closures are skipped. + * + * @param array> $tokens + * @param int $stackPtr + * + * @return bool + */ + protected function isInsideAnonClass(array $tokens, int $stackPtr): bool + { + if (empty($tokens[$stackPtr]['conditions'])) { + return false; + } + + foreach ($tokens[$stackPtr]['conditions'] as $code) { + if ($code === T_ANON_CLASS) { + return true; + } + } + + return false; + } + /** * Check if the previous line is "complete" (ends with statement terminator or closing brace). * If not, the next line might be a continuation. diff --git a/tests/_data/ConsistentIndent/after.php b/tests/_data/ConsistentIndent/after.php index dd03f2f..6f54790 100644 --- a/tests/_data/ConsistentIndent/after.php +++ b/tests/_data/ConsistentIndent/after.php @@ -108,4 +108,21 @@ public function nullCoalesceShouldNotBeFlagged($params): ?int ?? $params['home_id'] ?? null; } + + public function anonClassAsArgumentShouldNotBeFlagged(array $payload): object + { + return new Service( + new class ($payload) extends Base { + public function __construct(private array $payload) + { + parent::__construct(); + } + + public function defaultProvider(): string + { + return 'codex'; + } + }, + ); + } } diff --git a/tests/_data/ConsistentIndent/before.php b/tests/_data/ConsistentIndent/before.php index c193d8b..49b8c09 100644 --- a/tests/_data/ConsistentIndent/before.php +++ b/tests/_data/ConsistentIndent/before.php @@ -108,4 +108,21 @@ public function nullCoalesceShouldNotBeFlagged($params): ?int ?? $params['home_id'] ?? null; } + + public function anonClassAsArgumentShouldNotBeFlagged(array $payload): object + { + return new Service( + new class ($payload) extends Base { + public function __construct(private array $payload) + { + parent::__construct(); + } + + public function defaultProvider(): string + { + return 'codex'; + } + }, + ); + } }