From 1bb942e7b0a2ab95022351a728c201953acee369 Mon Sep 17 00:00:00 2001 From: kenny-ish <114967656+kenny-ish@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:05:24 +0000 Subject: [PATCH] [TypeDeclaration] Skip already documented generic type in AddParamTypeSplFixedArrayRector The rule compares the existing @param type with the GenericObjectType it wants to add. Tokens is not a generic class, so an existing "Tokens" tag resolves to "iterable&Tokens", which never equals the generic object type, and the tag is rewritten with fully qualified names. With php-cs-fixer's fully_qualified_strict_types shortening it back, the two tools never settle. Skip a param whose docblock already has a single-argument generic tag that resolves to the same fixed array class and item class, whatever the spelling of the names. Fixes rectorphp/rector#9933 --- .../Fixture/plain_doc.php.inc | 35 ++++++++++++++++ .../skip_same_namespace_generic_doc.php.inc | 13 ++++++ .../skip_short_named_generic_doc.php.inc | 16 +++++++ .../AddParamTypeSplFixedArrayRector.php | 42 +++++++++++++++++++ stubs/PhpCsFixer/Tokenizer/Token.php | 13 ++++++ stubs/PhpCsFixer/Tokenizer/Tokens.php | 18 ++++++++ 6 files changed, 137 insertions(+) create mode 100644 rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/plain_doc.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/skip_same_namespace_generic_doc.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/skip_short_named_generic_doc.php.inc create mode 100644 stubs/PhpCsFixer/Tokenizer/Token.php create mode 100644 stubs/PhpCsFixer/Tokenizer/Tokens.php diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/plain_doc.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/plain_doc.php.inc new file mode 100644 index 00000000000..a3857be6ec5 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/plain_doc.php.inc @@ -0,0 +1,35 @@ + +----- + $tokens + */ + public function someFunction(Tokens $tokens) + { + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/skip_same_namespace_generic_doc.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/skip_same_namespace_generic_doc.php.inc new file mode 100644 index 00000000000..fc78be8ba33 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/skip_same_namespace_generic_doc.php.inc @@ -0,0 +1,13 @@ + $tokens + */ + public function someFunction(Tokens $tokens) + { + } +} diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/skip_short_named_generic_doc.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/skip_short_named_generic_doc.php.inc new file mode 100644 index 00000000000..e74a36b73a7 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector/Fixture/skip_short_named_generic_doc.php.inc @@ -0,0 +1,16 @@ + $tokens + */ + public function someFunction(Tokens $tokens) + { + } +} diff --git a/rules/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector.php b/rules/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector.php index 3e71d234e88..c0810c95ebd 100644 --- a/rules/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector.php +++ b/rules/TypeDeclaration/Rector/FunctionLike/AddParamTypeSplFixedArrayRector.php @@ -6,17 +6,22 @@ use PhpParser\Node; use PhpParser\Node\FunctionLike; +use PhpParser\Node\Param; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger; use Rector\Rector\AbstractRector; use Rector\RuleDoc\CodeSample\CodeSample; use Rector\RuleDoc\RuleDefinition; +use Rector\StaticTypeMapper\StaticTypeMapper; /** * @see \Rector\Tests\TypeDeclaration\Rector\FunctionLike\AddParamTypeSplFixedArrayRector\AddParamTypeSplFixedArrayRectorTest @@ -34,6 +39,7 @@ final class AddParamTypeSplFixedArrayRector extends AbstractRector public function __construct( private readonly PhpDocTypeChanger $phpDocTypeChanger, private readonly PhpDocInfoFactory $phpDocInfoFactory, + private readonly StaticTypeMapper $staticTypeMapper, ) { } @@ -118,6 +124,11 @@ public function refactor(Node $node): ?Node $functionLikePhpDocInfo ??= $this->phpDocInfoFactory->createFromNodeOrEmpty($node); $paramName = $this->getName($param); + + if ($this->isAlreadyDocumented($functionLikePhpDocInfo, $param, $paramName, $genericParamType)) { + continue; + } + $changedParamType = $this->phpDocTypeChanger->changeParamType( $node, $functionLikePhpDocInfo, @@ -138,6 +149,37 @@ public function refactor(Node $node): ?Node return null; } + /** + * An existing "Tokens" docblock, with short or fully qualified names, resolves to "iterable&Tokens", + * as the fixed array class is not generic itself. That never equals the generic object type, so the tag would be + * rewritten with fully qualified names on every run. + */ + private function isAlreadyDocumented( + PhpDocInfo $phpDocInfo, + Param $param, + string $paramName, + GenericObjectType $genericObjectType + ): bool { + $paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramName); + if (! $paramTagValueNode instanceof ParamTagValueNode) { + return false; + } + + $genericTypeNode = $paramTagValueNode->type; + if (! $genericTypeNode instanceof GenericTypeNode || count($genericTypeNode->genericTypes) !== 1) { + return false; + } + + $currentType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($genericTypeNode->type, $param); + $currentItemType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType( + $genericTypeNode->genericTypes[0], + $param + ); + + return $currentType->equals(new ObjectType($genericObjectType->getClassName())) + && $currentItemType->equals($genericObjectType->getTypes()[0]); + } + private function resolveGenericType(TypeWithClassName $typeWithClassName): GenericObjectType|null { foreach (self::SPL_FIXED_ARRAY_TO_SINGLE as $fixedArrayClass => $singleClass) { diff --git a/stubs/PhpCsFixer/Tokenizer/Token.php b/stubs/PhpCsFixer/Tokenizer/Token.php new file mode 100644 index 00000000000..fc3248f2bc0 --- /dev/null +++ b/stubs/PhpCsFixer/Tokenizer/Token.php @@ -0,0 +1,13 @@ + + */ +class Tokens extends SplFixedArray +{ +}