mirror of
https://github.com/rectorphp/rector.git
synced 2024-06-07 03:40:50 +00:00
257 lines
8.2 KiB
PHP
257 lines
8.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Rector\TypeDeclaration\Rector\FunctionLike;
|
|
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\FunctionLike;
|
|
use PhpParser\Node\IntersectionType;
|
|
use PhpParser\Node\Name;
|
|
use PhpParser\Node\NullableType;
|
|
use PhpParser\Node\Stmt\Class_;
|
|
use PhpParser\Node\Stmt\ClassMethod;
|
|
use PhpParser\Node\Stmt\Function_;
|
|
use PhpParser\Node\UnionType as PhpParserUnionType;
|
|
use PHPStan\Type\MixedType;
|
|
use PHPStan\Type\Type;
|
|
use PHPStan\Type\UnionType;
|
|
use Rector\Core\Php\PhpVersionProvider;
|
|
use Rector\Core\Rector\AbstractRector;
|
|
use Rector\Core\ValueObject\PhpVersionFeature;
|
|
use Rector\NodeTypeResolver\Node\AttributeKey;
|
|
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
|
|
use Rector\TypeDeclaration\PhpDocParser\NonInformativeReturnTagRemover;
|
|
use Rector\TypeDeclaration\PhpParserTypeAnalyzer;
|
|
use Rector\TypeDeclaration\TypeAlreadyAddedChecker\ReturnTypeAlreadyAddedChecker;
|
|
use Rector\TypeDeclaration\TypeAnalyzer\ObjectTypeComparator;
|
|
use Rector\TypeDeclaration\TypeInferer\ReturnTypeInferer;
|
|
use Rector\TypeDeclaration\TypeInferer\ReturnTypeInferer\ReturnTypeDeclarationReturnTypeInfererTypeInferer;
|
|
use Rector\VendorLocker\NodeVendorLocker\ClassMethodReturnTypeOverrideGuard;
|
|
use Rector\VendorLocker\VendorLockResolver;
|
|
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
|
|
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
|
|
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
|
|
|
|
/**
|
|
* @changelog https://wiki.php.net/rfc/scalar_type_hints_v5
|
|
*
|
|
* @see \Rector\Tests\TypeDeclaration\Rector\FunctionLike\ReturnTypeDeclarationRector\ReturnTypeDeclarationRectorTest
|
|
*/
|
|
final class ReturnTypeDeclarationRector extends AbstractRector implements MinPhpVersionInterface
|
|
{
|
|
public function __construct(
|
|
private readonly ReturnTypeInferer $returnTypeInferer,
|
|
private readonly ReturnTypeAlreadyAddedChecker $returnTypeAlreadyAddedChecker,
|
|
private readonly NonInformativeReturnTagRemover $nonInformativeReturnTagRemover,
|
|
private readonly ClassMethodReturnTypeOverrideGuard $classMethodReturnTypeOverrideGuard,
|
|
private readonly VendorLockResolver $vendorLockResolver,
|
|
private readonly PhpParserTypeAnalyzer $phpParserTypeAnalyzer,
|
|
private readonly ObjectTypeComparator $objectTypeComparator,
|
|
private readonly PhpVersionProvider $phpVersionProvider,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @return array<class-string<Node>>
|
|
*/
|
|
public function getNodeTypes(): array
|
|
{
|
|
return [Function_::class, ClassMethod::class];
|
|
}
|
|
|
|
public function getRuleDefinition(): RuleDefinition
|
|
{
|
|
return new RuleDefinition(
|
|
'Change @return types and type from static analysis to type declarations if not a BC-break',
|
|
[
|
|
new CodeSample(
|
|
<<<'CODE_SAMPLE'
|
|
class SomeClass
|
|
{
|
|
/**
|
|
* @return int
|
|
*/
|
|
public function getCount()
|
|
{
|
|
}
|
|
}
|
|
CODE_SAMPLE
|
|
,
|
|
<<<'CODE_SAMPLE'
|
|
class SomeClass
|
|
{
|
|
public function getCount(): int
|
|
{
|
|
}
|
|
}
|
|
CODE_SAMPLE
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param ClassMethod|Function_ $node
|
|
*/
|
|
public function refactor(Node $node): ?Node
|
|
{
|
|
if ($this->shouldSkipClassLike($node)) {
|
|
return null;
|
|
}
|
|
|
|
if ($node instanceof ClassMethod && $this->shouldSkipClassMethod($node)) {
|
|
return null;
|
|
}
|
|
|
|
$inferedReturnType = $this->returnTypeInferer->inferFunctionLikeWithExcludedInferers(
|
|
$node,
|
|
[ReturnTypeDeclarationReturnTypeInfererTypeInferer::class]
|
|
);
|
|
|
|
if ($inferedReturnType instanceof MixedType) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->returnTypeAlreadyAddedChecker->isSameOrBetterReturnTypeAlreadyAdded($node, $inferedReturnType)) {
|
|
return null;
|
|
}
|
|
|
|
if (! $inferedReturnType instanceof UnionType) {
|
|
return $this->processType($node, $inferedReturnType);
|
|
}
|
|
|
|
foreach ($inferedReturnType->getTypes() as $unionedType) {
|
|
// mixed type cannot be joined with another types
|
|
if ($unionedType instanceof MixedType) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return $this->processType($node, $inferedReturnType);
|
|
}
|
|
|
|
public function provideMinPhpVersion(): int
|
|
{
|
|
return PhpVersionFeature::SCALAR_TYPES;
|
|
}
|
|
|
|
private function processType(ClassMethod | Function_ $node, Type $inferedType): ?Node
|
|
{
|
|
$inferredReturnNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode(
|
|
$inferedType,
|
|
TypeKind::RETURN()
|
|
);
|
|
|
|
// nothing to change in PHP code
|
|
if (! $inferredReturnNode instanceof Node) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->shouldSkipInferredReturnNode($node)) {
|
|
return null;
|
|
}
|
|
|
|
// should be previous overridden?
|
|
if ($node->returnType !== null && $this->shouldSkipExistingReturnType($node, $inferedType)) {
|
|
return null;
|
|
}
|
|
|
|
/** @var Name|NullableType|PhpParserUnionType $inferredReturnNode */
|
|
$this->addReturnType($node, $inferredReturnNode);
|
|
$this->nonInformativeReturnTagRemover->removeReturnTagIfNotUseful($node);
|
|
|
|
return $node;
|
|
}
|
|
|
|
private function shouldSkipClassMethod(ClassMethod $classMethod): bool
|
|
{
|
|
if ($this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($classMethod)) {
|
|
return true;
|
|
}
|
|
|
|
return $this->vendorLockResolver->isReturnChangeVendorLockedIn($classMethod);
|
|
}
|
|
|
|
private function shouldSkipInferredReturnNode(ClassMethod | Function_ $functionLike): bool
|
|
{
|
|
// already overridden by previous populateChild() method run
|
|
if ($functionLike->returnType === null) {
|
|
return false;
|
|
}
|
|
|
|
return (bool) $functionLike->returnType->getAttribute(AttributeKey::DO_NOT_CHANGE);
|
|
}
|
|
|
|
private function shouldSkipExistingReturnType(ClassMethod | Function_ $functionLike, Type $inferedType): bool
|
|
{
|
|
if ($functionLike->returnType === null) {
|
|
return false;
|
|
}
|
|
|
|
if ($functionLike instanceof ClassMethod && $this->vendorLockResolver->isReturnChangeVendorLockedIn(
|
|
$functionLike
|
|
)) {
|
|
return true;
|
|
}
|
|
|
|
$currentType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($functionLike->returnType);
|
|
if ($this->objectTypeComparator->isCurrentObjectTypeSubType($currentType, $inferedType)) {
|
|
return true;
|
|
}
|
|
|
|
return $this->isNullableTypeSubType($currentType, $inferedType);
|
|
}
|
|
|
|
private function addReturnType(
|
|
ClassMethod | Function_ $functionLike,
|
|
Name|NullableType|\PhpParser\Node\UnionType|IntersectionType $inferredReturnNode
|
|
): void {
|
|
if ($functionLike->returnType === null) {
|
|
$functionLike->returnType = $inferredReturnNode;
|
|
return;
|
|
}
|
|
|
|
$isSubtype = $this->phpParserTypeAnalyzer->isCovariantSubtypeOf($inferredReturnNode, $functionLike->returnType);
|
|
if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::COVARIANT_RETURN) && $isSubtype) {
|
|
$functionLike->returnType = $inferredReturnNode;
|
|
return;
|
|
}
|
|
|
|
if (! $isSubtype) {
|
|
// type override with correct one
|
|
$functionLike->returnType = $inferredReturnNode;
|
|
}
|
|
}
|
|
|
|
private function isNullableTypeSubType(Type $currentType, Type $inferedType): bool
|
|
{
|
|
if (! $currentType instanceof UnionType) {
|
|
return false;
|
|
}
|
|
|
|
if (! $inferedType instanceof UnionType) {
|
|
return false;
|
|
}
|
|
|
|
// probably more/less strict union type on purpose
|
|
if ($currentType->isSubTypeOf($inferedType)
|
|
->yes()) {
|
|
return true;
|
|
}
|
|
|
|
return $inferedType->isSubTypeOf($currentType)
|
|
->yes();
|
|
}
|
|
|
|
private function shouldSkipClassLike(FunctionLike $functionLike): bool
|
|
{
|
|
if (! $functionLike instanceof ClassMethod) {
|
|
return false;
|
|
}
|
|
|
|
$classLike = $this->betterNodeFinder->findParentType($functionLike, Class_::class);
|
|
return ! $classLike instanceof Class_;
|
|
}
|
|
}
|