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
85 changes: 64 additions & 21 deletions PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
use PhpCollective\Traits\CommentingTrait;

/**
* Method doc blocks should have a consistent order of tag types.
* Function/method and class/interface/trait doc blocks should have a consistent order of tag types.
*
* @author Mark Scherer
* @license MIT
Expand All @@ -23,24 +23,54 @@ class DocBlockTagOrderSniff extends AbstractSniff
use CommentingTrait;

/**
* All other tags will go above those
* Tag order for function/method docblocks. All other tags will go above those.
*
* Configurable via XML:
*
* ``` xml
* <rule ref="PhpCollective.Commenting.DocBlockTagOrder">
* <properties>
* <property name="order" type="array" value="deprecated,see,param,throws,return"/>
* </properties>
* </rule>
* ```
*
* Note: leading "at" sign on tag names is optional; it will be normalized.
*
* @var array<int, string>
*/
protected array $order = [
public array $order = [
'@deprecated',
'@see',
'@param',
'@throws',
'@return',
];

/**
* Tag order for class/interface/trait docblocks. All other tags will go above those.
*
* Configurable via XML the same way as `$order`.
*
* @var array<int, string>
*/
public array $classOrder = [
'@template',
'@extends',
'@implements',
'@property',
'@property-read',
'@property-write',
'@method',
'@mixin',
];

/**
* @inheritDoc
*/
public function register(): array
{
return [T_FUNCTION];
return [T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT];
}

/**
Expand All @@ -49,11 +79,14 @@ public function register(): array
public function process(File $phpcsFile, $stackPtr): void
{
$tokens = $phpcsFile->getTokens();
$isFunction = $tokens[$stackPtr]['code'] === T_FUNCTION;

// Don't mess with closures
$prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
if (!$this->isGivenKind(Tokens::$methodPrefixes, $tokens[$prevIndex])) {
return;
if ($isFunction) {
// Don't mess with closures
$prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
if (!$this->isGivenKind(Tokens::$methodPrefixes, $tokens[$prevIndex])) {
return;
}
}

$docBlockEndIndex = $this->findRelatedDocBlock($phpcsFile, $stackPtr);
Expand All @@ -63,20 +96,23 @@ public function process(File $phpcsFile, $stackPtr): void

$docBlockStartIndex = $tokens[$docBlockEndIndex]['comment_opener'];

$order = $isFunction ? $this->order : $this->classOrder;

$tags = $this->readTags($phpcsFile, $docBlockStartIndex, $docBlockEndIndex);
$tags = $this->checkAnnotationTagOrder($tags);
$tags = $this->checkAnnotationTagOrder($tags, $order);

$this->fixOrder($phpcsFile, $docBlockStartIndex, $docBlockEndIndex, $tags);
$this->fixOrder($phpcsFile, $docBlockStartIndex, $docBlockEndIndex, $tags, $order);
}

/**
* @param array<int, array<string, mixed>> $tags
* @param array<int, string> $orderList
*
* @return array<int, array<string, mixed>>
*/
protected function checkAnnotationTagOrder(array $tags): array
protected function checkAnnotationTagOrder(array $tags, array $orderList): array
{
$order = $this->getTagOrderMap();
$order = $this->getTagOrderMap($orderList);

$currentOrder = null;
foreach ($tags as $i => $tag) {
Expand Down Expand Up @@ -153,13 +189,12 @@ protected function getEndIndex(array $tokens, int $index): int
$index++;
}

// Jump to the previous line
$currentLine = $tokens[$index]['line'];
while ($tokens[$index]['line'] === $currentLine) {
// Walk back to the line that actually contains this tag's content,
// skipping over any blank " * " separator lines so they are not folded
// into this tag's range and re-emitted in the rebuilt docblock.
while ($index > $startIndex && $tokens[$index]['code'] !== T_DOC_COMMENT_STRING && $tokens[$index]['code'] !== T_DOC_COMMENT_TAG) {
$index--;
}
// Fix for single line doc blocks
$index = max($index, $startIndex);

return $this->getLastTokenOfLine($tokens, $index);
}
Expand Down Expand Up @@ -206,10 +241,11 @@ protected function getContent(array $tokens, int $start, int $end): string
* @param int $docBlockStartIndex
* @param int $docBlockEndIndex
* @param array<int, array<string, mixed>> $tags
* @param array<int, string> $orderList
*
* @return void
*/
protected function fixOrder(File $phpcsFile, int $docBlockStartIndex, int $docBlockEndIndex, array $tags): void
protected function fixOrder(File $phpcsFile, int $docBlockStartIndex, int $docBlockEndIndex, array $tags, array $orderList): void
{
$errors = [];
foreach ($tags as $i => $tag) {
Expand All @@ -231,7 +267,7 @@ protected function fixOrder(File $phpcsFile, int $docBlockStartIndex, int $docBl

$phpcsFile->fixer->beginChangeset();

$order = $this->getTagOrderMap();
$order = $this->getTagOrderMap($orderList);

$newOrder = [];
foreach ($tags as $tag) {
Expand Down Expand Up @@ -262,10 +298,17 @@ protected function fixOrder(File $phpcsFile, int $docBlockStartIndex, int $docBl
}

/**
* @param array<int, string> $orderList
*
* @return array<string, int>
*/
protected function getTagOrderMap(): array
protected function getTagOrderMap(array $orderList): array
{
return array_flip($this->order);
$normalized = [];
foreach ($orderList as $tag) {
$normalized[] = str_starts_with($tag, '@') ? $tag : '@' . $tag;
}

return array_flip($normalized);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/**
* MIT License
* For full license information, please view the LICENSE file that was distributed with this source code.
*/

namespace PhpCollective\Test\PhpCollective\Sniffs\Commenting;

use PhpCollective\Sniffs\Commenting\DocBlockTagOrderSniff;
use PhpCollective\Test\TestCase;

class DocBlockTagOrderSniffTest extends TestCase
{
/**
* @return void
*/
public function testDocBlockTagOrderSniffer(): void
{
$sniff = new DocBlockTagOrderSniff();
// One error per misordered docblock: class-level + function-level.
$this->assertSnifferFindsFixableErrors($sniff, 2, 2);
}

/**
* @return void
*/
public function testDocBlockTagOrderFixer(): void
{
$sniff = new DocBlockTagOrderSniff();
$this->assertSnifferCanFixErrors($sniff, 2);
}

/**
* Custom $classOrder set via property override (mirrors XML configuration).
*
* @return void
*/
public function testDocBlockTagOrderCustomClassOrder(): void
{
$this->prefix = 'custom-order.';

$sniff = new DocBlockTagOrderSniff();
$sniff->classOrder = ['@mixin', '@method', '@property', '@extends'];

$this->assertSnifferCanFixErrors($sniff, 1);

$this->prefix = null;
}

/**
* Tag names without leading "@" should be normalized.
*
* @return void
*/
public function testDocBlockTagOrderCustomClassOrderWithoutAtPrefix(): void
{
$this->prefix = 'custom-order.';

$sniff = new DocBlockTagOrderSniff();
$sniff->classOrder = ['mixin', 'method', 'property', 'extends'];

$this->assertSnifferCanFixErrors($sniff, 1);

$this->prefix = null;
}

/**
* Blank-line separators between tag groups should be normalized away when tags are reordered,
* otherwise an orphan blank line may end up splitting a now-coherent same-kind tag group.
*
* @return void
*/
public function testDocBlockTagOrderNormalizesBlankLineSeparators(): void
{
$this->prefix = 'blank-lines.';

$sniff = new DocBlockTagOrderSniff();

$this->assertSnifferCanFixErrors($sniff, 1);

$this->prefix = null;
}
}
28 changes: 28 additions & 0 deletions tests/_data/DocBlockTagOrder/after.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace PhpCollective;

/**
* Class-level docblock with scrambled tag groups.
*
* @author Some Author
* @extends \BaseTable<array{Slugged: \Behavior, Tree: \Behavior}>
* @property \Foo $Boards
* @property \Foo $LastUsers
* @method \Foo save(\Bar $entity, array $options = [])
* @method \Foo get(mixed $primaryKey, array $finder = 'all')
* @mixin \BehaviorOne
* @mixin \BehaviorTwo
*/
class FixMe
{
/**
* @param string $foo
* @throws \RuntimeException
* @return string
*/
public function badOrder(string $foo): string
{
return $foo;
}
}
28 changes: 28 additions & 0 deletions tests/_data/DocBlockTagOrder/before.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace PhpCollective;

/**
* Class-level docblock with scrambled tag groups.
*
* @author Some Author
* @method \Foo save(\Bar $entity, array $options = [])
* @property \Foo $Boards
* @mixin \BehaviorOne
* @extends \BaseTable<array{Slugged: \Behavior, Tree: \Behavior}>
* @method \Foo get(mixed $primaryKey, array $finder = 'all')
* @property \Foo $LastUsers
* @mixin \BehaviorTwo
*/
class FixMe
{
/**
* @return string
* @param string $foo
* @throws \RuntimeException
*/
public function badOrder(string $foo): string
{
return $foo;
}
}
17 changes: 17 additions & 0 deletions tests/_data/DocBlockTagOrder/blank-lines.after.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace PhpCollective;

/**
* Class with blank-line separators between tag groups.
*
* @extends \BaseTable
* @property \Foo $LastUsers
* @method \Foo first()
* @method \Foo second()
* @method \Foo third()
* @mixin \Bar
*/
class FixMe
{
}
19 changes: 19 additions & 0 deletions tests/_data/DocBlockTagOrder/blank-lines.before.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);

namespace PhpCollective;

/**
* Class with blank-line separators between tag groups.
*
* @method \Foo first()
*
* @mixin \Bar
*
* @property \Foo $LastUsers
* @method \Foo second()
* @extends \BaseTable
* @method \Foo third()
*/
class FixMe
{
}
13 changes: 13 additions & 0 deletions tests/_data/DocBlockTagOrder/custom-order.after.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace PhpCollective;

/**
* @mixin \Behavior
* @method \Foo bar()
* @property \Foo $X
* @extends \BaseTable
*/
class FixMe
{
}
13 changes: 13 additions & 0 deletions tests/_data/DocBlockTagOrder/custom-order.before.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace PhpCollective;

/**
* @extends \BaseTable
* @property \Foo $X
* @method \Foo bar()
* @mixin \Behavior
*/
class FixMe
{
}
Loading