From 8e19a42db829b2368729804973127c0d377a7659 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 27 May 2026 05:19:53 +0200 Subject: [PATCH] Align native union type order with docblocks --- .../Commenting/DocBlockTagOrderSniff.php | 2 +- .../Sniffs/Commenting/TypeHintSniff.php | 117 +++++++++++++++++- docs/README.md | 4 + .../Sniffs/Commenting/TypeHintSniffTest.php | 2 +- tests/_data/TypeHint/after.php | 12 ++ tests/_data/TypeHint/before.php | 12 ++ 6 files changed, 146 insertions(+), 3 deletions(-) diff --git a/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php b/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php index 569b855..396435f 100644 --- a/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php +++ b/PhpCollective/Sniffs/Commenting/DocBlockTagOrderSniff.php @@ -664,7 +664,7 @@ protected function scoreSubject(string $subject, array $prefixes): int * * @return array */ - protected function normalizeInnerPrefixes(string|array|null $value): array + protected function normalizeInnerPrefixes(array|string|null $value): array { if ($value === null || $value === '') { return []; diff --git a/PhpCollective/Sniffs/Commenting/TypeHintSniff.php b/PhpCollective/Sniffs/Commenting/TypeHintSniff.php index 688358f..4ce8993 100644 --- a/PhpCollective/Sniffs/Commenting/TypeHintSniff.php +++ b/PhpCollective/Sniffs/Commenting/TypeHintSniff.php @@ -32,6 +32,8 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use const T_FUNCTION; +use const T_TYPE_UNION; /** * Verifies order of types in type hints. Also removes duplicates. @@ -91,7 +93,10 @@ class TypeHintSniff extends AbstractSniff */ public function register(): array { - return [T_DOC_COMMENT_OPEN_TAG]; + return [ + T_DOC_COMMENT_OPEN_TAG, + T_FUNCTION, + ]; } /** @@ -101,6 +106,12 @@ public function register(): array */ public function process(File $phpcsFile, $stackPtr): void { + if ($phpcsFile->getTokens()[$stackPtr]['code'] === T_FUNCTION) { + $this->processNativeTypeHints($phpcsFile, $stackPtr); + + return; + } + $tokens = $phpcsFile->getTokens(); if (!isset($tokens[$stackPtr]['comment_closer'])) { @@ -244,6 +255,81 @@ public function process(File $phpcsFile, $stackPtr): void } } + /** + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $stackPtr + * + * @return void + */ + protected function processNativeTypeHints(File $phpcsFile, int $stackPtr): void + { + foreach ($phpcsFile->getMethodParameters($stackPtr) as $param) { + if ($param['type_hint'] === '' || $param['type_hint_token'] === false || $param['type_hint_end_token'] === false) { + continue; + } + + $this->processNativeTypeHint( + $phpcsFile, + $param['type_hint_token'], + $param['type_hint_end_token'], + $param['type_hint'], + ); + } + + $methodProperties = $phpcsFile->getMethodProperties($stackPtr); + if ( + $methodProperties['return_type'] === '' || + $methodProperties['return_type_token'] === false || + $methodProperties['return_type_end_token'] === false + ) { + return; + } + + $this->processNativeTypeHint( + $phpcsFile, + $methodProperties['return_type_token'], + $methodProperties['return_type_end_token'], + $methodProperties['return_type'], + ); + } + + /** + * @param \PHP_CodeSniffer\Files\File $phpcsFile + * @param int $typeHintToken + * @param int $typeHintEndToken + * @param string $typeHint + * + * @return void + */ + protected function processNativeTypeHint(File $phpcsFile, int $typeHintToken, int $typeHintEndToken, string $typeHint): void + { + if (!str_contains($typeHint, '|') || !$this->contains($phpcsFile, T_TYPE_UNION, $typeHintToken, $typeHintEndToken, false)) { + return; + } + + $sortedTypeHint = $this->getSortedNativeTypeHint(explode('|', $typeHint)); + if ($sortedTypeHint === $typeHint) { + return; + } + + $fix = $phpcsFile->addFixableError( + 'Native type hint is not formatted properly, expected "%s"', + $typeHintToken, + 'IncorrectNativeFormat', + [$sortedTypeHint], + ); + if (!$fix) { + return; + } + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($typeHintToken, $sortedTypeHint); + for ($i = $typeHintToken + 1; $i <= $typeHintEndToken; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + /** * @param array<\PHPStan\PhpDocParser\Ast\Type\TypeNode> $types node types * @@ -301,6 +387,35 @@ protected function getSortedTypeHint(array $types): string return $this->renderUnionTypes($types); } + /** + * @param array $types + * + * @return string + */ + protected function getSortedNativeTypeHint(array $types): string + { + $sortable = array_fill_keys(static::$sortMap, []); + $unsortable = []; + foreach ($types as $type) { + $sortName = strtolower(ltrim($type, '\\')); + if (in_array($sortName, static::$sortMap, true)) { + $sortable[$sortName][] = $type; + } else { + $unsortable[] = $type; + } + } + + $sorted = []; + array_walk($sortable, function ($types) use (&$sorted): void { + $sorted = array_merge($sorted, $types); + }); + + $types = array_merge($unsortable, $sorted); + $types = $this->makeUnique($types); + + return implode('|', $types); + } + /** * @param array<\PHPStan\PhpDocParser\Ast\Type\TypeNode|string> $types * diff --git a/docs/README.md b/docs/README.md index 1611052..9efd287 100644 --- a/docs/README.md +++ b/docs/README.md @@ -98,6 +98,10 @@ To enable checking for (and auto-fixing) invalid `: void` return types on these ``` +## Union type order +`PhpCollective.Commenting.TypeHint` sorts docblock union types by the standard type order. +Native function parameter and return union types are sorted with the same order, keeping signatures such as `string|int $modelId` aligned with `@param string|int $modelId`. + ## Customize DocBlock tag ordering `PhpCollective.Commenting.DocBlockTagOrder` enforces a consistent tag order in docblocks for functions, methods, classes, interfaces, and traits. Three properties can be overridden via XML. diff --git a/tests/PhpCollective/Sniffs/Commenting/TypeHintSniffTest.php b/tests/PhpCollective/Sniffs/Commenting/TypeHintSniffTest.php index 07299df..d5402fb 100644 --- a/tests/PhpCollective/Sniffs/Commenting/TypeHintSniffTest.php +++ b/tests/PhpCollective/Sniffs/Commenting/TypeHintSniffTest.php @@ -17,7 +17,7 @@ class TypeHintSniffTest extends TestCase */ public function testTypeHintSniffer(): void { - $this->assertSnifferFindsErrors(new TypeHintSniff(), 10); + $this->assertSnifferFindsErrors(new TypeHintSniff(), 12); } /** diff --git a/tests/_data/TypeHint/after.php b/tests/_data/TypeHint/after.php index 5faef54..8306dbd 100644 --- a/tests/_data/TypeHint/after.php +++ b/tests/_data/TypeHint/after.php @@ -32,6 +32,18 @@ public function second(array $test): array return []; } + /** + * @param string|int $modelId + */ + public function nativeUnionOrder(string|int $modelId): void + { + } + + public function nativeUnionReturn(): string|int + { + return 1; + } + /** * @param \ArrayObject|int[] $array * diff --git a/tests/_data/TypeHint/before.php b/tests/_data/TypeHint/before.php index 0b7c237..3d177fa 100644 --- a/tests/_data/TypeHint/before.php +++ b/tests/_data/TypeHint/before.php @@ -33,6 +33,18 @@ public function second(array $test): array return []; } + /** + * @param string|int $modelId + */ + public function nativeUnionOrder(int|string $modelId): void + { + } + + public function nativeUnionReturn(): int|string + { + return 1; + } + /** * @param \ArrayObject|int[] $array *