Skip to content

Commit f2d468d

Browse files
Philosoftclaude
andcommitted
[DeadCode] Skip RemoveReturnTagIncompatibleWithNativeTypeRector on a type alias nested in a union
A @phpstan-type alias name is not resolved to the type it stands for, it becomes a NonExistingObjectType. So "@return ConfigArray|CustomConfig" over a native "array" looked like a contradiction and the tag was removed. The alias guard only matched when the whole @return type was a single IdentifierTypeNode. Look the name up in every identifier of the type node instead, which covers unions, nullables and intersections as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 02ca77e commit f2d468d

3 files changed

Lines changed: 84 additions & 2 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\ClassMethod\RemoveReturnTagIncompatibleWithNativeTypeRector\Fixture;
4+
5+
/**
6+
* @phpstan-type ConfigArray array{is_dry: bool}
7+
*/
8+
final class RemoveObjectReturnNextToTypeAlias
9+
{
10+
/**
11+
* @return \stdClass
12+
*/
13+
public function getName(): string
14+
{
15+
return 'name';
16+
}
17+
}
18+
19+
?>
20+
-----
21+
<?php
22+
23+
namespace Rector\Tests\DeadCode\Rector\ClassMethod\RemoveReturnTagIncompatibleWithNativeTypeRector\Fixture;
24+
25+
/**
26+
* @phpstan-type ConfigArray array{is_dry: bool}
27+
*/
28+
final class RemoveObjectReturnNextToTypeAlias
29+
{
30+
public function getName(): string
31+
{
32+
return 'name';
33+
}
34+
}
35+
36+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Rector\Tests\DeadCode\Rector\ClassMethod\RemoveReturnTagIncompatibleWithNativeTypeRector\Fixture;
4+
5+
/**
6+
* @phpstan-type ConfigArray array{is_dry: bool}
7+
* @phpstan-type CustomConfig array{}
8+
*/
9+
interface SkipUnionOfPHPStanTypeAliases
10+
{
11+
/** @return ConfigArray|CustomConfig */
12+
public function getConfig(): array;
13+
}

‎rules/DeadCode/Rector/ClassMethod/RemoveReturnTagIncompatibleWithNativeTypeRector.php‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@
99
use PhpParser\Node\Stmt\Function_;
1010
use PHPStan\Analyser\Scope;
1111
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
12+
use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
13+
use PHPStan\PhpDoc\Tag\TypeAliasTag;
14+
use PHPStan\PhpDocParser\Ast\Node as AstNode;
1215
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
1316
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
1417
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1518
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
19+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
1620
use PHPStan\Reflection\ClassReflection;
1721
use PHPStan\Type\ObjectType;
1822
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
1923
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
2024
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
25+
use Rector\PhpDocParser\PhpDocParser\PhpDocNodeTraverser;
2126
use Rector\PHPStan\ScopeFetcher;
2227
use Rector\Rector\AbstractRector;
2328
use Rector\StaticTypeMapper\StaticTypeMapper;
@@ -163,8 +168,36 @@ private function isClassTypeAlias(Scope $scope, ReturnTagValueNode $returnTagVal
163168
return false;
164169
}
165170

166-
return $returnTagValueNode->type instanceof IdentifierTypeNode
167-
&& isset($typeAliases[$returnTagValueNode->type->name]);
171+
return $this->containsTypeAliasName($returnTagValueNode->type, $typeAliases);
172+
}
173+
174+
/**
175+
* The alias can be nested in a composed type as well, e.g. "ConfigArray|CustomConfig" or "?ConfigArray"
176+
*
177+
* @param array<string, TypeAliasTag|TypeAliasImportTag> $typeAliases
178+
*/
179+
private function containsTypeAliasName(TypeNode $typeNode, array $typeAliases): bool
180+
{
181+
if ($typeNode instanceof IdentifierTypeNode) {
182+
return isset($typeAliases[$typeNode->name]);
183+
}
184+
185+
$hasTypeAliasName = false;
186+
187+
// the traverser visits sub-nodes only, that is why the type node itself is checked above
188+
$phpDocNodeTraverser = new PhpDocNodeTraverser();
189+
$phpDocNodeTraverser->traverseWithCallable($typeNode, '', static function (AstNode $astNode) use (
190+
$typeAliases,
191+
&$hasTypeAliasName
192+
): ?int {
193+
if ($astNode instanceof IdentifierTypeNode && isset($typeAliases[$astNode->name])) {
194+
$hasTypeAliasName = true;
195+
}
196+
197+
return null;
198+
});
199+
200+
return $hasTypeAliasName;
168201
}
169202

170203
private function isReturnTemplate(PhpDocInfo $phpDocInfo, ReturnTagValueNode $returnTagValueNode): bool

0 commit comments

Comments
 (0)