Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/Rules/DocBlockHeaderFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string|array<string>> $annotations
*/
Expand Down
105 changes: 105 additions & 0 deletions tests/src/Rules/DocBlockHeaderFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -845,4 +845,109 @@ public function testInsertNewDocBlockWithClassNameAndSeparateNone(): void
$expected = "<?php /**\n * TestClass.\n *\n * @author John Doe\n */class TestClass {}";
self::assertSame($expected, $tokens->generateCode());
}

public function testSkipsAnonymousClasses(): void
{
$code = '<?php $obj = new class {};';
$tokens = Tokens::fromCode($code);
$file = new SplFileInfo(__FILE__);

$method = new ReflectionMethod($this->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 = '<?php class RegularClass {} $obj = new class {};';
$tokens = Tokens::fromCode($code);
$file = new SplFileInfo(__FILE__);

$method = new ReflectionMethod($this->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 = '<?php $obj = new class {};';
$tokens = Tokens::fromCode($code);

$method = new ReflectionMethod($this->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 = '<?php class RegularClass {}';
$tokens = Tokens::fromCode($code);

$method = new ReflectionMethod($this->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 = '<?php $obj = new #[SomeAttribute] class {};';
$tokens = Tokens::fromCode($code);

$method = new ReflectionMethod($this->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);
}
}