diff --git a/src/Rules/DocBlockHeaderFixer.php b/src/Rules/DocBlockHeaderFixer.php index 8eb1344..5f3fcdd 100644 --- a/src/Rules/DocBlockHeaderFixer.php +++ b/src/Rules/DocBlockHeaderFixer.php @@ -121,11 +121,62 @@ 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 + $insideAttribute = false; + for ($i = $classIndex - 1; $i >= 0; --$i) { + $token = $tokens[$i]; + + // 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)) { + $insideAttribute = false; + continue; + } + + // Skip everything inside attributes + if ($insideAttribute) { + 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; + } + + // If we hit any other meaningful token, it's not anonymous + break; + } + + return false; + } + /** * @param array> $annotations */ 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); + } }