From 3cf1dec9b262bb6fbc55d426e73d6503316558e5 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 5 Jul 2026 14:48:33 +0200 Subject: [PATCH] [DeadCode] Keep generic @var/@return self<...> narrowing over instantiated class Fixes rectorphp/rector#9793 --- .../Fixture/skip_generic_return_self.php.inc | 18 +++++++++++++ .../skip_generic_var_on_new_self.php.inc | 16 +++++++++++ ...moveDuplicatedReturnSelfDocblockRector.php | 6 +++++ .../RemoveNonExistingVarAnnotationRector.php | 27 +++++++++++++++++-- 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveDuplicatedReturnSelfDocblockRector/Fixture/skip_generic_return_self.php.inc create mode 100644 rules-tests/DeadCode/Rector/Node/RemoveNonExistingVarAnnotationRector/Fixture/skip_generic_var_on_new_self.php.inc diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveDuplicatedReturnSelfDocblockRector/Fixture/skip_generic_return_self.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveDuplicatedReturnSelfDocblockRector/Fixture/skip_generic_return_self.php.inc new file mode 100644 index 00000000000..952bcaabf0b --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveDuplicatedReturnSelfDocblockRector/Fixture/skip_generic_return_self.php.inc @@ -0,0 +1,18 @@ + + */ + public function success(): self + { + return $this; + } +} diff --git a/rules-tests/DeadCode/Rector/Node/RemoveNonExistingVarAnnotationRector/Fixture/skip_generic_var_on_new_self.php.inc b/rules-tests/DeadCode/Rector/Node/RemoveNonExistingVarAnnotationRector/Fixture/skip_generic_var_on_new_self.php.inc new file mode 100644 index 00000000000..5102a9a5e3a --- /dev/null +++ b/rules-tests/DeadCode/Rector/Node/RemoveNonExistingVarAnnotationRector/Fixture/skip_generic_var_on_new_self.php.inc @@ -0,0 +1,16 @@ + */ + return new self(); + } +} diff --git a/rules/DeadCode/Rector/ClassMethod/RemoveDuplicatedReturnSelfDocblockRector.php b/rules/DeadCode/Rector/ClassMethod/RemoveDuplicatedReturnSelfDocblockRector.php index 2cf76163778..62b4f8bb763 100644 --- a/rules/DeadCode/Rector/ClassMethod/RemoveDuplicatedReturnSelfDocblockRector.php +++ b/rules/DeadCode/Rector/ClassMethod/RemoveDuplicatedReturnSelfDocblockRector.php @@ -11,6 +11,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassReflection; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; @@ -126,6 +127,11 @@ private function isCurrentObjectReturnDocType( ): bool { $docType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($typeNode, $classMethod); + // generic narrowing, e.g. @return self is not a plain duplicate of the native self type + if ($docType instanceof GenericObjectType) { + return false; + } + // covers @return $this and @return static if ($docType instanceof StaticType) { return $docType->getClassName() === $classReflection->getName(); diff --git a/rules/DeadCode/Rector/Node/RemoveNonExistingVarAnnotationRector.php b/rules/DeadCode/Rector/Node/RemoveNonExistingVarAnnotationRector.php index bee534d6154..dc8e6302363 100644 --- a/rules/DeadCode/Rector/Node/RemoveNonExistingVarAnnotationRector.php +++ b/rules/DeadCode/Rector/Node/RemoveNonExistingVarAnnotationRector.php @@ -22,6 +22,7 @@ use PhpParser\Node\Stmt\Switch_; use PhpParser\Node\Stmt\While_; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\Comments\NodeDocBlock\DocBlockUpdater; @@ -157,7 +158,7 @@ public function refactor(Node $node): ?Node $variableName = ltrim($varTagValueNode->variableName, '$'); - if ($variableName === '' && $this->isAllowedEmptyVariableName($stmt)) { + if ($variableName === '' && $this->isAllowedEmptyVariableName($stmt, $varTagValueNode)) { continue; } @@ -252,12 +253,34 @@ private function isObjectShapePseudoType(VarTagValueNode $varTagValueNode): bool return str_contains($varTagValueNode->description, '}'); } - private function isAllowedEmptyVariableName(Stmt $stmt): bool + private function isAllowedEmptyVariableName(Stmt $stmt, VarTagValueNode $varTagValueNode): bool { if ($stmt instanceof Return_ && $stmt->expr instanceof CallLike && ! $stmt->expr instanceof New_) { return true; } + // generic/template narrowing over the instantiated class, e.g. /** @var self */ return new self(...) + if ($stmt instanceof Return_ && $stmt->expr instanceof New_ && $this->isGenericTypeOfNewClass( + $stmt->expr, + $varTagValueNode + )) { + return true; + } + return $stmt instanceof Expression && $stmt->expr instanceof Assign && $stmt->expr->var instanceof Variable; } + + private function isGenericTypeOfNewClass(New_ $new, VarTagValueNode $varTagValueNode): bool + { + if (! $varTagValueNode->type instanceof GenericTypeNode) { + return false; + } + + $newClassName = $this->getName($new->class); + if ($newClassName === null) { + return false; + } + + return strcasecmp($varTagValueNode->type->type->name, $newClassName) === 0; + } }