|
20 | 20 | use PhpParser\Node\UnionType; |
21 | 21 | use PhpParser\NodeVisitor; |
22 | 22 | use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; |
| 23 | +use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; |
23 | 24 | use PHPStan\Reflection\ClassReflection; |
24 | 25 | use PHPStan\Type\MixedType; |
25 | 26 | use PHPStan\Type\TypeCombinator; |
| 27 | +use PHPStan\Type\TypeWithClassName; |
| 28 | +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; |
26 | 29 | use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; |
27 | 30 | use Rector\Contract\Rector\ConfigurableRectorInterface; |
28 | 31 | use Rector\Naming\PropertyRenamer\PropertyPromotionRenamer; |
@@ -213,6 +216,10 @@ public function refactor(Node $node): ?Node |
213 | 216 | continue; |
214 | 217 | } |
215 | 218 |
|
| 219 | + if ($this->shouldSkipNarrowingVarDoc($property, $param, $constructorPhpDocInfo, $paramName)) { |
| 220 | + continue; |
| 221 | + } |
| 222 | + |
216 | 223 | $hasChanged = true; |
217 | 224 |
|
218 | 225 | // remove property from class |
@@ -388,6 +395,49 @@ private function shouldSkipPropertyOrParam(Property $property, Param $param): bo |
388 | 395 | && ! $this->nodeComparator->areNodesEqual($property->type, $param->type); |
389 | 396 | } |
390 | 397 |
|
| 398 | + /** |
| 399 | + * A class-typed property may carry a narrowing @var (e.g. native AdapterInterface, @var CacheProvider) used to |
| 400 | + * type calls on the property. Promotion would drop that @var (it is not preserved as a @param here), $property so skip to |
| 401 | + * avoid losing type information. |
| 402 | + */ |
| 403 | + private function shouldSkipNarrowingVarDoc( |
| 404 | + Property $property, |
| 405 | + Param $param, |
| 406 | + PhpDocInfo $constructorPhpDocInfo, |
| 407 | + string $paramName |
| 408 | + ): bool { |
| 409 | + if (! $property->type instanceof Node || ! $param->type instanceof Node) { |
| 410 | + return false; |
| 411 | + } |
| 412 | + |
| 413 | + // an explicit @param already carries the type onto the promoted property |
| 414 | + if ($constructorPhpDocInfo->getParamTagValueByName($paramName) instanceof ParamTagValueNode) { |
| 415 | + return false; |
| 416 | + } |
| 417 | + |
| 418 | + $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); |
| 419 | + $varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode(); |
| 420 | + if (! $varTagValueNode instanceof VarTagValueNode) { |
| 421 | + return false; |
| 422 | + } |
| 423 | + |
| 424 | + // a description keeps the docblock around anyway |
| 425 | + if ($varTagValueNode->description !== '') { |
| 426 | + return false; |
| 427 | + } |
| 428 | + |
| 429 | + $varType = $this->staticTypeMapper->mapPHPStanPhpDocTypeToPHPStanType($varTagValueNode, $property); |
| 430 | + |
| 431 | + // only class-typed @var narrowing is lost; generics, arrays and int ranges are preserved on promotion |
| 432 | + if (! $varType instanceof TypeWithClassName) { |
| 433 | + return false; |
| 434 | + } |
| 435 | + |
| 436 | + $paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); |
| 437 | + |
| 438 | + return ! $this->typeComparator->areTypesEqual($varType, $paramType); |
| 439 | + } |
| 440 | + |
391 | 441 | private function shouldRemoveNullFromForPromotedParamType(Property $property, Param $param): bool |
392 | 442 | { |
393 | 443 | if (! $property->type instanceof Node || ! $param->type instanceof Node) { |
|
0 commit comments