diff --git a/packages/phpstan-static-type-mapper/src/PHPStanStaticTypeMapper.php b/packages/phpstan-static-type-mapper/src/PHPStanStaticTypeMapper.php index 8b706c789bc..aebc45786db 100644 --- a/packages/phpstan-static-type-mapper/src/PHPStanStaticTypeMapper.php +++ b/packages/phpstan-static-type-mapper/src/PHPStanStaticTypeMapper.php @@ -16,6 +16,16 @@ use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface; final class PHPStanStaticTypeMapper { + /** + * @var string + */ + public const KIND_PARAM = 'param'; + + /** + * @var string + */ + public const KIND_PROPERTY = 'property'; + /** * @var TypeMapperInterface[] */ diff --git a/packages/phpstan-static-type-mapper/src/TypeMapper/ClassStringTypeMapper.php b/packages/phpstan-static-type-mapper/src/TypeMapper/ClassStringTypeMapper.php index f519f353d55..41236b576ff 100644 --- a/packages/phpstan-static-type-mapper/src/TypeMapper/ClassStringTypeMapper.php +++ b/packages/phpstan-static-type-mapper/src/TypeMapper/ClassStringTypeMapper.php @@ -35,6 +35,9 @@ final class ClassStringTypeMapper implements TypeMapperInterface return null; } + /** + * @param ClassStringType $type + */ public function mapToDocString(Type $type, ?Type $parentType = null): string { return $type->describe(VerbosityLevel::typeOnly()); diff --git a/phpstan.neon b/phpstan.neon index 968754537af..8ea66653163 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,7 +16,9 @@ parameters: - compiler/src paths: + - bin - src + - rules - packages - tests - compiler/src @@ -232,3 +234,7 @@ parameters: - '#Method Rector\\SOLID\\Reflection\\ParentConstantReflectionResolver\:\:(.*?)\(\) should return ReflectionClassConstant\|null but returns ReflectionClassConstant\|false#' - '#Parameter \#1 \$firstStmt of method Rector\\Core\\Rector\\MethodBody\\NormalToFluentRector\:\:isBothMethodCallMatch\(\) expects PhpParser\\Node\\Stmt\\Expression, PhpParser\\Node\\Stmt given#' - '#Method Rector\\Core\\Rector\\AbstractRector\:\:wrapToArg\(\) should return array but returns array#' + - '#Property PhpParser\\Node\\Stmt\\ClassMethod\:\:\$returnType \(PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\|PhpParser\\Node\\UnionType\|null\) does not accept PhpParser\\Node#' + + - '#Parameter \#1 \$possibleSubtype of method Rector\\TypeDeclaration\\PhpParserTypeAnalyzer\:\:isSubtypeOf\(\) expects PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\|PhpParser\\Node\\UnionType, PhpParser\\Node given#' + - '#Parameter \#2 \$inferredReturnNode of method Rector\\TypeDeclaration\\Rector\\FunctionLike\\ReturnTypeDeclarationRector\:\:addReturnType\(\) expects PhpParser\\Node, PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\|PhpParser\\Node\\UnionType\|null given#' diff --git a/phpunit.xml b/phpunit.xml index c410aa3bf7d..9e8a7f53834 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,6 +10,7 @@ + rules/*/tests packages/*/tests tests @@ -17,6 +18,7 @@ + rules/*/src packages/*/src src diff --git a/rules/php-74/src/Rector/Property/TypedPropertyRector.php b/rules/php-74/src/Rector/Property/TypedPropertyRector.php index 994a198d4b2..f078f310af4 100644 --- a/rules/php-74/src/Rector/Property/TypedPropertyRector.php +++ b/rules/php-74/src/Rector/Property/TypedPropertyRector.php @@ -15,6 +15,7 @@ use Rector\Core\RectorDefinition\CodeSample; use Rector\Core\RectorDefinition\RectorDefinition; use Rector\Core\ValueObject\PhpVersionFeature; use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper; use Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer; use Rector\VendorLocker\VendorLockResolver; @@ -95,7 +96,10 @@ PHP return null; } - $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($varType, 'property'); + $propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $varType, + PHPStanStaticTypeMapper::KIND_PROPERTY + ); if ($propertyTypeNode === null) { return null; } diff --git a/rules/type-declaration/src/Rector/FunctionLike/ParamTypeDeclarationRector.php b/rules/type-declaration/src/Rector/FunctionLike/ParamTypeDeclarationRector.php index 4380490147b..b26ee031328 100644 --- a/rules/type-declaration/src/Rector/FunctionLike/ParamTypeDeclarationRector.php +++ b/rules/type-declaration/src/Rector/FunctionLike/ParamTypeDeclarationRector.php @@ -5,18 +5,23 @@ declare(strict_types=1); namespace Rector\TypeDeclaration\Rector\FunctionLike; use PhpParser\Node; +use PhpParser\Node\FunctionLike; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\NullableType; +use PhpParser\Node\Param; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; +use PhpParser\Node\UnionType; use PHPStan\Type\Type; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; use Rector\Core\RectorDefinition\CodeSample; use Rector\Core\RectorDefinition\RectorDefinition; use Rector\Core\ValueObject\PhpVersionFeature; use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper; /** * @see \Rector\TypeDeclaration\Tests\Rector\FunctionLike\ParamTypeDeclarationRector\ParamTypeDeclarationRectorTest @@ -114,70 +119,7 @@ PHP return null; } - foreach ($node->params as $position => $paramNode) { - // skip variadics - if ($paramNode->variadic) { - continue; - } - - // already set → skip - $hasNewType = false; - if ($paramNode->type !== null) { - $hasNewType = $paramNode->type->getAttribute(self::HAS_NEW_INHERITED_TYPE, false); - if (! $hasNewType) { - continue; - } - } - - $paramNodeName = '$' . $this->getName($paramNode->var); - - // no info about it - if (! isset($paramWithTypes[$paramNodeName])) { - continue; - } - - $paramType = $paramWithTypes[$paramNodeName]; - $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($paramType, 'param'); - if ($paramTypeNode === null) { - continue; - } - - $position = (int) $position; - if ($node instanceof ClassMethod && $this->vendorLockResolver->isParamChangeVendorLockedIn( - $node, - $position - )) { - continue; - } - - if ($hasNewType) { - // should override - is it subtype? - $possibleOverrideNewReturnType = $paramTypeNode; - if ($possibleOverrideNewReturnType !== null) { - if ($paramNode->type === null) { - $paramNode->type = $paramTypeNode; - } elseif ($this->phpParserTypeAnalyzer->isSubtypeOf( - $possibleOverrideNewReturnType, - $paramNode->type - )) { - // allow override - $paramNode->type = $paramTypeNode; - } - } - } else { - $paramNode->type = $paramTypeNode; - - $paramNodeType = $paramNode->type instanceof NullableType ? $paramNode->type->type : $paramNode->type; - // "resource" is valid phpdoc type, but it's not implemented in PHP - if ($paramNodeType instanceof Name && reset($paramNodeType->parts) === 'resource') { - $paramNode->type = null; - - continue; - } - } - - $this->populateChildren($node, $position, $paramType); - } + $this->refactorParams($node, $paramWithTypes); return $node; } @@ -250,4 +192,111 @@ PHP $this->notifyNodeChangeFileInfo($paramNode); } + + /** + * @param ClassMethod|Function_ $functionLike + * @param Type[] $paramWithTypes + */ + private function refactorParams(FunctionLike $functionLike, array $paramWithTypes): void + { + foreach ($functionLike->params as $position => $param) { + // to be sure + $position = (int) $position; + + if ($this->shouldSkipParam($param)) { + continue; + } + + $hasNewType = false; + + $docParamType = $this->matchParamNodeFromDoc($paramWithTypes, $param); + if ($docParamType === null) { + continue; + } + + $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $docParamType, + PHPStanStaticTypeMapper::KIND_PARAM + ); + + if ($paramTypeNode === null) { + continue; + } + + if ($functionLike instanceof ClassMethod && $this->vendorLockResolver->isParamChangeVendorLockedIn( + $functionLike, + $position + )) { + continue; + } + + $this->changeParamNodeType($hasNewType, $paramTypeNode, $param); + + $this->populateChildren($functionLike, $position, $docParamType); + } + } + + private function isResourceType(Node $node): bool + { + if ($this->isName($node, 'resource')) { + return true; + } + + if ($node instanceof NullableType) { + return $this->isResourceType($node->type); + } + + return false; + } + + /** + * @param Identifier|Name|NullableType|UnionType $paramTypeNode + */ + private function changeParamNodeType(bool $hasNewType, Node $paramTypeNode, Param $param): void + { + if ($hasNewType) { + // should override - is it subtype? + if ($param->type === null) { + $param->type = $paramTypeNode; + } elseif ($this->phpParserTypeAnalyzer->isSubtypeOf($paramTypeNode, $param->type)) { + // allow override + $param->type = $paramTypeNode; + } + + return; + } + + if ($this->isResourceType($paramTypeNode)) { + // "resource" is valid phpdoc type, but it's not implemented in PHP + $param->type = null; + } else { + $param->type = $paramTypeNode; + } + } + + /** + * @param Type[] $paramWithTypes + */ + private function matchParamNodeFromDoc(array $paramWithTypes, Param $param): ?Type + { + $paramNodeName = '$' . $this->getName($param->var); + + return $paramWithTypes[$paramNodeName] ?? null; + } + + private function shouldSkipParam(Param $param): bool + { + if ($param->variadic) { + return true; + } + + // already set → skip + if ($param->type === null) { + return false; + } + + $hasNewType = $param->type->getAttribute(self::HAS_NEW_INHERITED_TYPE, false); + + return ! $hasNewType; + } }