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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

</div>

This packages contains a [PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) rule to automatically fix the class header regarding PHP DocBlocks.
This packages contains a [PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) rule to automatically fix the header regarding PHP DocBlocks for classes, interfaces, traits and enums.

> [!warning]
> This package is in early development stage and may change significantly in the future. Use it at your own risk.
Expand All @@ -26,6 +26,10 @@ class MyClass
// ...
}
}

interface MyInterface {}
trait MyTrait {}
enum MyEnum {}
```

**After:**
Expand Down Expand Up @@ -76,7 +80,7 @@ return (new PhpCsFixer\Config())
],
'preserve_existing' => true,
'separate' => 'none',
'add_class_name' => true,
'add_structure_name' => true,
],
])
;
Expand All @@ -100,7 +104,7 @@ return (new PhpCsFixer\Config())
],
preserveExisting: true,
separate: \KonradMichalik\PhpDocBlockHeaderFixer\Enum\Separate::None,
addClassName: true
addStructureName: true
)->__toArray()
])
;
Expand Down
8 changes: 4 additions & 4 deletions src/Generators/DocBlockHeader.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private function __construct(
public readonly array $annotations,
public readonly bool $preserveExisting,
public readonly Separate $separate,
public readonly bool $addClassName,
public readonly bool $addStructureName,
) {}

/**
Expand All @@ -45,11 +45,11 @@ public static function create(
array $annotations,
bool $preserveExisting = true,
Separate $separate = Separate::Both,
bool $addClassName = false,
bool $addStructureName = false,
): self {
self::validateAnnotations($annotations);

return new self($annotations, $preserveExisting, $separate, $addClassName);
return new self($annotations, $preserveExisting, $separate, $addStructureName);
}

/**
Expand All @@ -62,7 +62,7 @@ public function __toArray(): array
'annotations' => $this->annotations,
'preserve_existing' => $this->preserveExisting,
'separate' => $this->separate->value,
'add_class_name' => $this->addClassName,
'add_structure_name' => $this->addStructureName,
],
];
}
Expand Down
75 changes: 39 additions & 36 deletions src/Rules/DocBlockHeaderFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ final class DocBlockHeaderFixer extends AbstractFixer implements ConfigurableFix
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Add configurable DocBlock annotations before class declarations.',
'Add configurable DocBlock annotations before class, interface, trait, and enum declarations.',
[],
);
}
Expand All @@ -63,7 +63,10 @@ public function getName(): string

public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_CLASS);
return $tokens->isTokenKindFound(T_CLASS)
|| $tokens->isTokenKindFound(T_INTERFACE)
|| $tokens->isTokenKindFound(T_TRAIT)
|| $tokens->isTokenKindFound(T_ENUM);
}

public function getConfigurationDefinition(): FixerConfigurationResolverInterface
Expand All @@ -81,7 +84,7 @@ public function getConfigurationDefinition(): FixerConfigurationResolverInterfac
->setAllowedValues(Separate::getList())
->setDefault(Separate::None->value)
->getOption(),
(new FixerOptionBuilder('add_class_name', 'Add class name before annotations'))
(new FixerOptionBuilder('add_structure_name', 'Add structure name before annotations'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
Expand All @@ -103,45 +106,45 @@ protected function applyFix(SplFileInfo $file, Tokens $tokens): void
for ($index = 0, $limit = $tokens->count(); $index < $limit; ++$index) {
$token = $tokens[$index];

if (!$token->isGivenKind(T_CLASS)) {
if (!$token->isGivenKind([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM])) {
continue;
}

$className = $this->getClassName($tokens, $index);
$this->processClassDocBlock($tokens, $index, $annotations, $className);
$structureName = $this->getStructureName($tokens, $index);
$this->processStructureDocBlock($tokens, $index, $annotations, $structureName);
}
}

/**
* @param array<string, string|array<string>> $annotations
*/
private function processClassDocBlock(Tokens $tokens, int $classIndex, array $annotations, string $className): void
private function processStructureDocBlock(Tokens $tokens, int $structureIndex, array $annotations, string $structureName): void
{
$existingDocBlockIndex = $this->findExistingDocBlock($tokens, $classIndex);
$existingDocBlockIndex = $this->findExistingDocBlock($tokens, $structureIndex);
$preserveExisting = $this->resolvedConfiguration['preserve_existing'] ?? true;

if (null !== $existingDocBlockIndex) {
if ($preserveExisting) {
$this->mergeWithExistingDocBlock($tokens, $existingDocBlockIndex, $annotations, $className);
$this->mergeWithExistingDocBlock($tokens, $existingDocBlockIndex, $annotations, $structureName);
} else {
$this->replaceDocBlock($tokens, $existingDocBlockIndex, $annotations, $className);
$this->replaceDocBlock($tokens, $existingDocBlockIndex, $annotations, $structureName);
}
} else {
$this->insertNewDocBlock($tokens, $classIndex, $annotations, $className);
$this->insertNewDocBlock($tokens, $structureIndex, $annotations, $structureName);
}
}

private function getClassName(Tokens $tokens, int $classIndex): string
private function getStructureName(Tokens $tokens, int $structureIndex): string
{
// Look for the class name token after the 'class' keyword
for ($i = $classIndex + 1, $limit = $tokens->count(); $i < $limit; ++$i) {
// Look for the structure name token after the keyword (class/interface/trait/enum)
for ($i = $structureIndex + 1, $limit = $tokens->count(); $i < $limit; ++$i) {
$token = $tokens[$i];

if ($token->isWhitespace()) {
continue;
}

// The first non-whitespace token after 'class' should be the class name
// The first non-whitespace token after the keyword should be the structure name
if ($token->isGivenKind(T_STRING)) {
return $token->getContent();
}
Expand All @@ -153,9 +156,9 @@ private function getClassName(Tokens $tokens, int $classIndex): string
return '';
}

private function findExistingDocBlock(Tokens $tokens, int $classIndex): ?int
private function findExistingDocBlock(Tokens $tokens, int $structureIndex): ?int
{
for ($i = $classIndex - 1; $i >= 0; --$i) {
for ($i = $structureIndex - 1; $i >= 0; --$i) {
$token = $tokens[$i];

if ($token->isWhitespace()) {
Expand All @@ -178,32 +181,32 @@ private function findExistingDocBlock(Tokens $tokens, int $classIndex): ?int
/**
* @param array<string, string|array<string>> $annotations
*/
private function mergeWithExistingDocBlock(Tokens $tokens, int $docBlockIndex, array $annotations, string $className): void
private function mergeWithExistingDocBlock(Tokens $tokens, int $docBlockIndex, array $annotations, string $structureName): void
{
$existingContent = $tokens[$docBlockIndex]->getContent();
$existingAnnotations = $this->parseExistingAnnotations($existingContent);
$mergedAnnotations = $this->mergeAnnotations($existingAnnotations, $annotations);

$newDocBlock = $this->buildDocBlock($mergedAnnotations, $className);
$newDocBlock = $this->buildDocBlock($mergedAnnotations, $structureName);
$tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $newDocBlock]);
}

/**
* @param array<string, string|array<string>> $annotations
*/
private function replaceDocBlock(Tokens $tokens, int $docBlockIndex, array $annotations, string $className): void
private function replaceDocBlock(Tokens $tokens, int $docBlockIndex, array $annotations, string $structureName): void
{
$newDocBlock = $this->buildDocBlock($annotations, $className);
$newDocBlock = $this->buildDocBlock($annotations, $structureName);
$tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $newDocBlock]);
}

/**
* @param array<string, string|array<string>> $annotations
*/
private function insertNewDocBlock(Tokens $tokens, int $classIndex, array $annotations, string $className): void
private function insertNewDocBlock(Tokens $tokens, int $structureIndex, array $annotations, string $structureName): void
{
$separate = $this->resolvedConfiguration['separate'] ?? 'none';
$insertIndex = $this->findInsertPosition($tokens, $classIndex);
$insertIndex = $this->findInsertPosition($tokens, $structureIndex);

$tokensToInsert = [];

Expand All @@ -213,14 +216,14 @@ private function insertNewDocBlock(Tokens $tokens, int $classIndex, array $annot
}

// Add the DocBlock
$docBlock = $this->buildDocBlock($annotations, $className);
$docBlock = $this->buildDocBlock($annotations, $structureName);
$tokensToInsert[] = new Token([T_DOC_COMMENT, $docBlock]);

// For compatibility with no_blank_lines_after_phpdoc, only add bottom separation when 'separate' is not 'none'
// This prevents conflicts with PHP-CS-Fixer rules that manage DocBlock spacing
if (in_array($separate, ['bottom', 'both'], true)) {
// Check if there's already whitespace after the class declaration
$nextToken = $tokens[$classIndex] ?? null;
// Check if there's already whitespace after the structure declaration
$nextToken = $tokens[$structureIndex] ?? null;
if (null !== $nextToken && !$nextToken->isWhitespace()) {
$tokensToInsert[] = new Token([T_WHITESPACE, "\n"]);
}
Expand All @@ -229,12 +232,12 @@ private function insertNewDocBlock(Tokens $tokens, int $classIndex, array $annot
$tokens->insertAt($insertIndex, $tokensToInsert);
}

private function findInsertPosition(Tokens $tokens, int $classIndex): int
private function findInsertPosition(Tokens $tokens, int $structureIndex): int
{
$insertIndex = $classIndex;
$insertIndex = $structureIndex;

// Look backwards for attributes, final, abstract keywords
for ($i = $classIndex - 1; $i >= 0; --$i) {
for ($i = $structureIndex - 1; $i >= 0; --$i) {
$token = $tokens[$i];

if ($token->isWhitespace()) {
Expand Down Expand Up @@ -287,21 +290,21 @@ private function mergeAnnotations(array $existing, array $new): array
/**
* @param array<string, string|array<string>> $annotations
*/
private function buildDocBlock(array $annotations, string $className): string
private function buildDocBlock(array $annotations, string $structureName): string
{
$addClassName = $this->resolvedConfiguration['add_class_name'] ?? false;
$addStructureName = $this->resolvedConfiguration['add_structure_name'] ?? false;

if (empty($annotations) && !$addClassName) {
if (empty($annotations) && !$addStructureName) {
return "/**\n */";
}

$docBlock = "/**\n";

// Add class name with dot if configured
if ($addClassName && !empty($className)) {
$docBlock .= " * {$className}.\n";
// Add structure name with dot if configured
if ($addStructureName && !empty($structureName)) {
$docBlock .= " * {$structureName}.\n";

// Add empty line after class name if there are annotations - compatible with phpdoc_separation
// Add empty line after structure name if there are annotations - compatible with phpdoc_separation
if (!empty($annotations)) {
$docBlock .= " *\n";
}
Expand Down
12 changes: 6 additions & 6 deletions tests/src/Generators/DocBlockHeaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function testToArrayReturnsCorrectStructure(): void
'annotations' => $annotations,
'preserve_existing' => false,
'separate' => 'top',
'add_class_name' => false,
'add_structure_name' => false,
],
];

Expand All @@ -110,7 +110,7 @@ public function testToArrayWithDefaultParameters(): void
'annotations' => $annotations,
'preserve_existing' => true,
'separate' => 'both',
'add_class_name' => false,
'add_structure_name' => false,
],
];

Expand Down Expand Up @@ -270,7 +270,7 @@ public function testClassIsFinal(): void
self::assertTrue($reflection->isFinal());
}

public function testCreateWithAddClassName(): void
public function testCreateWithAddStructureName(): void
{
$annotations = ['author' => 'John Doe'];
$docBlockHeader = DocBlockHeader::create(
Expand All @@ -283,10 +283,10 @@ public function testCreateWithAddClassName(): void
self::assertSame($annotations, $docBlockHeader->annotations);
self::assertTrue($docBlockHeader->preserveExisting);
self::assertSame(Separate::None, $docBlockHeader->separate);
self::assertTrue($docBlockHeader->addClassName);
self::assertTrue($docBlockHeader->addStructureName);
}

public function testToArrayWithAddClassName(): void
public function testToArrayWithAddStructureName(): void
{
$annotations = ['author' => 'John Doe'];
$docBlockHeader = DocBlockHeader::create(
Expand All @@ -303,7 +303,7 @@ public function testToArrayWithAddClassName(): void
'annotations' => $annotations,
'preserve_existing' => false,
'separate' => 'top',
'add_class_name' => true,
'add_structure_name' => true,
],
];

Expand Down
Loading