diff --git a/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php b/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php
index f527b4d..9042f3e 100644
--- a/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php
+++ b/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php
@@ -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
@@ -23,11 +23,23 @@ 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
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Note: leading "at" sign on tag names is optional; it will be normalized.
*
* @var array
*/
- protected array $order = [
+ public array $order = [
'@deprecated',
'@see',
'@param',
@@ -35,12 +47,30 @@ class DocBlockTagOrderSniff extends AbstractSniff
'@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
+ */
+ 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];
}
/**
@@ -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);
@@ -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> $tags
+ * @param array $orderList
*
* @return array>
*/
- 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) {
@@ -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);
}
@@ -206,10 +241,11 @@ protected function getContent(array $tokens, int $start, int $end): string
* @param int $docBlockStartIndex
* @param int $docBlockEndIndex
* @param array> $tags
+ * @param array $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) {
@@ -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) {
@@ -262,10 +298,17 @@ protected function fixOrder(File $phpcsFile, int $docBlockStartIndex, int $docBl
}
/**
+ * @param array $orderList
+ *
* @return array
*/
- 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);
}
}
diff --git a/tests/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniffTest.php b/tests/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniffTest.php
new file mode 100644
index 0000000..4f02afa
--- /dev/null
+++ b/tests/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniffTest.php
@@ -0,0 +1,84 @@
+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;
+ }
+}
diff --git a/tests/_data/DocBlockTagOrder/after.php b/tests/_data/DocBlockTagOrder/after.php
new file mode 100644
index 0000000..5ff3017
--- /dev/null
+++ b/tests/_data/DocBlockTagOrder/after.php
@@ -0,0 +1,28 @@
+
+ * @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;
+ }
+}
diff --git a/tests/_data/DocBlockTagOrder/before.php b/tests/_data/DocBlockTagOrder/before.php
new file mode 100644
index 0000000..aa5fe1d
--- /dev/null
+++ b/tests/_data/DocBlockTagOrder/before.php
@@ -0,0 +1,28 @@
+
+ * @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;
+ }
+}
diff --git a/tests/_data/DocBlockTagOrder/blank-lines.after.php b/tests/_data/DocBlockTagOrder/blank-lines.after.php
new file mode 100644
index 0000000..01c2899
--- /dev/null
+++ b/tests/_data/DocBlockTagOrder/blank-lines.after.php
@@ -0,0 +1,17 @@
+