From 75ce032bd07e9734001a8cee9aebc37e16fc4d68 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Wed, 1 Oct 2025 15:05:04 +0200 Subject: [PATCH 1/4] feat: add support for skipping anonymous classes in DocBlock processing --- src/Rules/DocBlockHeaderFixer.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Rules/DocBlockHeaderFixer.php b/src/Rules/DocBlockHeaderFixer.php index 8eb1344..4807e45 100644 --- a/src/Rules/DocBlockHeaderFixer.php +++ b/src/Rules/DocBlockHeaderFixer.php @@ -121,11 +121,39 @@ protected function applyFix(SplFileInfo $file, Tokens $tokens): void continue; } + // Skip anonymous classes (preceded by 'new' keyword) + if ($token->isGivenKind(T_CLASS) && $this->isAnonymousClass($tokens, $index)) { + continue; + } + $structureName = $this->getStructureName($tokens, $index); $this->processStructureDocBlock($tokens, $index, $annotations, $structureName); } } + private function isAnonymousClass(Tokens $tokens, int $classIndex): bool + { + // Look backwards for 'new' keyword + for ($i = $classIndex - 1; $i >= 0; --$i) { + $token = $tokens[$i]; + + // Skip whitespace and attributes + if ($token->isWhitespace() || $token->isGivenKind(T_ATTRIBUTE)) { + continue; + } + + // If we find 'new', it's an anonymous class + if ($token->isGivenKind(T_NEW)) { + return true; + } + + // If we hit any other meaningful token, it's not anonymous + break; + } + + return false; + } + /** * @param array> $annotations */ From 48b016afc0f0949a3f8c462fbcdb3f8d690e285b Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Wed, 1 Oct 2025 15:10:26 +0200 Subject: [PATCH 2/4] test: add tests to skip DocBlock processing for anonymous classes --- tests/src/Rules/DocBlockHeaderFixerTest.php | 105 ++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/tests/src/Rules/DocBlockHeaderFixerTest.php b/tests/src/Rules/DocBlockHeaderFixerTest.php index 60cfa3f..c73fbf5 100644 --- a/tests/src/Rules/DocBlockHeaderFixerTest.php +++ b/tests/src/Rules/DocBlockHeaderFixerTest.php @@ -845,4 +845,109 @@ public function testInsertNewDocBlockWithClassNameAndSeparateNone(): void $expected = "generateCode()); } + + public function testSkipsAnonymousClasses(): void + { + $code = 'fixer, 'applyFix'); + + $this->fixer->configure([ + 'annotations' => ['author' => 'John Doe'], + 'separate' => 'none', + 'ensure_spacing' => false, + ]); + $method->invoke($this->fixer, $file, $tokens); + + // Code should remain unchanged - no DocBlock added to anonymous class + self::assertSame($code, $tokens->generateCode()); + } + + public function testSkipsAnonymousClassesButProcessesRegularClasses(): void + { + $code = 'fixer, 'applyFix'); + + $this->fixer->configure([ + 'annotations' => ['author' => 'John Doe'], + 'separate' => 'none', + 'ensure_spacing' => false, + ]); + $method->invoke($this->fixer, $file, $tokens); + + $result = $tokens->generateCode(); + + // Regular class should have DocBlock + self::assertStringContainsString("/**\n * @author John Doe\n */class RegularClass", $result); + // Anonymous class should NOT have DocBlock (should remain as "new class") + self::assertStringContainsString('new class {}', $result); + } + + public function testIsAnonymousClassDetectsAnonymousClass(): void + { + $code = 'fixer, 'isAnonymousClass'); + + // Find the class token index + $classIndex = null; + for ($i = 0; $i < $tokens->count(); ++$i) { + if ($tokens[$i]->isGivenKind(T_CLASS)) { + $classIndex = $i; + break; + } + } + + $result = $method->invoke($this->fixer, $tokens, $classIndex); + + self::assertTrue($result); + } + + public function testIsAnonymousClassReturnsFalseForRegularClass(): void + { + $code = 'fixer, 'isAnonymousClass'); + + // Find the class token index + $classIndex = null; + for ($i = 0; $i < $tokens->count(); ++$i) { + if ($tokens[$i]->isGivenKind(T_CLASS)) { + $classIndex = $i; + break; + } + } + + $result = $method->invoke($this->fixer, $tokens, $classIndex); + + self::assertFalse($result); + } + + public function testIsAnonymousClassWithAttribute(): void + { + $code = 'fixer, 'isAnonymousClass'); + + // Find the class token index + $classIndex = null; + for ($i = 0; $i < $tokens->count(); ++$i) { + if ($tokens[$i]->isGivenKind(T_CLASS)) { + $classIndex = $i; + break; + } + } + + $result = $method->invoke($this->fixer, $tokens, $classIndex); + + self::assertTrue($result); + } } From 3e12480f63ef3d4b810ab8b11e42c4c729084e8e Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Wed, 1 Oct 2025 15:10:30 +0200 Subject: [PATCH 3/4] fix: improve handling of attributes in anonymous class detection --- src/Rules/DocBlockHeaderFixer.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Rules/DocBlockHeaderFixer.php b/src/Rules/DocBlockHeaderFixer.php index 4807e45..4877a06 100644 --- a/src/Rules/DocBlockHeaderFixer.php +++ b/src/Rules/DocBlockHeaderFixer.php @@ -134,11 +134,29 @@ protected function applyFix(SplFileInfo $file, Tokens $tokens): void private function isAnonymousClass(Tokens $tokens, int $classIndex): bool { // Look backwards for 'new' keyword + $insideAttribute = false; for ($i = $classIndex - 1; $i >= 0; --$i) { $token = $tokens[$i]; - // Skip whitespace and attributes - if ($token->isWhitespace() || $token->isGivenKind(T_ATTRIBUTE)) { + // Skip whitespace + if ($token->isWhitespace()) { + continue; + } + + // When going backwards, ']' marks the end of an attribute (we enter it) + if (']' === $token->getContent()) { + $insideAttribute = true; + continue; + } + + // T_ATTRIBUTE '#[' marks the start of an attribute (we exit it when going backwards) + if ($token->isGivenKind([T_ATTRIBUTE, T_FINAL, T_READONLY])) { + $insideAttribute = false; + continue; + } + + // Skip everything inside attributes + if ($insideAttribute) { continue; } From 4b5d9bb00886e6f94247db05f8f5178dca74a4f4 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Wed, 1 Oct 2025 15:17:52 +0200 Subject: [PATCH 4/4] fix: refine handling of modifiers in anonymous class detection --- src/Rules/DocBlockHeaderFixer.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Rules/DocBlockHeaderFixer.php b/src/Rules/DocBlockHeaderFixer.php index 4877a06..5f3fcdd 100644 --- a/src/Rules/DocBlockHeaderFixer.php +++ b/src/Rules/DocBlockHeaderFixer.php @@ -150,7 +150,7 @@ private function isAnonymousClass(Tokens $tokens, int $classIndex): bool } // T_ATTRIBUTE '#[' marks the start of an attribute (we exit it when going backwards) - if ($token->isGivenKind([T_ATTRIBUTE, T_FINAL, T_READONLY])) { + if ($token->isGivenKind(T_ATTRIBUTE)) { $insideAttribute = false; continue; } @@ -160,6 +160,11 @@ private function isAnonymousClass(Tokens $tokens, int $classIndex): bool continue; } + // Skip modifiers that can appear between 'new' and 'class' + if ($token->isGivenKind([T_FINAL, T_READONLY])) { + continue; + } + // If we find 'new', it's an anonymous class if ($token->isGivenKind(T_NEW)) { return true;