Skip to content

Commit

Permalink
[ReturnTypeInferer] Drop this/static docblock type check on ReturnTyp…
Browse files Browse the repository at this point in the history
…eInferer (#6270)

* [ReturnTypeInferer] Drop this/static type check on ReturnTypeInferer

* use phpstan TypeCombinator
  • Loading branch information
samsonasik authored Sep 1, 2024
1 parent 1eb04d6 commit 03ea9b2
Show file tree
Hide file tree
Showing 3 changed files with 5 additions and 343 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,11 @@

namespace Rector\TypeDeclaration\TypeAnalyzer;

use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ArrayType;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use Rector\PHPStanStaticTypeMapper\TypeAnalyzer\UnionTypeAnalyzer;
use Rector\TypeDeclaration\NodeTypeAnalyzer\DetailedTypeAnalyzer;

final readonly class GenericClassStringTypeNormalizer
{
public function __construct(
private ReflectionProvider $reflectionProvider,
private DetailedTypeAnalyzer $detailedTypeAnalyzer,
private UnionTypeAnalyzer $unionTypeAnalyzer
) {
}

public function normalize(Type $type): ArrayType | UnionType | Type
{
$type = TypeTraverser::map($type, function (Type $type, $callback): Type {
if (! $type instanceof ConstantStringType) {
return $callback($type);
}

$value = $type->getValue();

// skip string that look like classe
if ($value === 'error') {
return $callback($type);
}

if (! $this->reflectionProvider->hasClass($value)) {
return $callback($type);
}

return $this->resolveStringType($value);
});

if ($type instanceof UnionType && ! $this->unionTypeAnalyzer->isNullable($type, true)) {
return $this->resolveClassStringInUnionType($type);
}

if ($type instanceof ArrayType && $type->getKeyType() instanceof UnionType) {
return $this->resolveArrayTypeWithUnionKeyType($type);
}

return $type;
}

public function isAllGenericClassStringType(UnionType $unionType): bool
{
foreach ($unionType->getTypes() as $type) {
Expand All @@ -70,69 +19,4 @@ public function isAllGenericClassStringType(UnionType $unionType): bool

return true;
}

private function resolveArrayTypeWithUnionKeyType(ArrayType $arrayType): ArrayType
{
$itemType = $arrayType->getItemType();

if (! $itemType instanceof UnionType) {
return $arrayType;
}

$keyType = $arrayType->getKeyType();
$isAllGenericClassStringType = $this->isAllGenericClassStringType($itemType);

if (! $isAllGenericClassStringType) {
return new ArrayType($keyType, new MixedType());
}

if ($this->detailedTypeAnalyzer->isTooDetailed($itemType)) {
return new ArrayType($keyType, new ClassStringType());
}

return $arrayType;
}

private function resolveClassStringInUnionType(UnionType $type): UnionType | ArrayType
{
$unionTypes = $type->getTypes();

foreach ($unionTypes as $unionType) {
if (! $unionType instanceof ArrayType) {
return $type;
}

$keyType = $unionType->getKeyType();
$itemType = $unionType->getItemType();

if (! $keyType instanceof MixedType && ! $keyType instanceof ConstantIntegerType) {
return $type;
}

if ($itemType instanceof ArrayType) {
$arrayType = new ArrayType(new MixedType(), new MixedType());
return new ArrayType($keyType, $arrayType);
}

if (! $itemType instanceof ClassStringType) {
return $type;
}
}

return new ArrayType(new MixedType(), new ClassStringType());
}

private function resolveStringType(string $value): GenericClassStringType | StringType
{
$classReflection = $this->reflectionProvider->getClass($value);
if ($classReflection->isBuiltin()) {
return new GenericClassStringType(new ObjectType($value));
}

if (str_contains($value, '\\')) {
return new GenericClassStringType(new ObjectType($value));
}

return new StringType();
}
}
214 changes: 2 additions & 212 deletions rules/TypeDeclaration/TypeInferer/ReturnTypeInferer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,13 @@

namespace Rector\TypeDeclaration\TypeInferer;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
use Rector\Enum\ObjectReference;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\Php\PhpVersionProvider;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\Reflection\ReflectionResolver;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Rector\TypeDeclaration\TypeAnalyzer\GenericClassStringTypeNormalizer;
use Rector\TypeDeclaration\TypeInferer\ReturnTypeInferer\ReturnedNodesReturnTypeInfererTypeInferer;
use Rector\TypeDeclaration\TypeNormalizer;
use Rector\ValueObject\PhpVersionFeature;

/**
* @internal
Expand All @@ -39,207 +19,17 @@
{
public function __construct(
private TypeNormalizer $typeNormalizer,
private ReturnedNodesReturnTypeInfererTypeInferer $returnedNodesReturnTypeInfererTypeInferer,
private GenericClassStringTypeNormalizer $genericClassStringTypeNormalizer,
private PhpVersionProvider $phpVersionProvider,
private BetterNodeFinder $betterNodeFinder,
private ReflectionResolver $reflectionResolver,
private ReflectionProvider $reflectionProvider,
private NodeTypeResolver $nodeTypeResolver
private ReturnedNodesReturnTypeInfererTypeInferer $returnedNodesReturnTypeInfererTypeInferer
) {
}

public function inferFunctionLike(ClassMethod|Function_|Closure $functionLike): Type
{
$isSupportedStaticReturnType = $this->phpVersionProvider->isAtLeastPhpVersion(
PhpVersionFeature::STATIC_RETURN_TYPE
);

$originalType = $this->returnedNodesReturnTypeInfererTypeInferer->inferFunctionLike($functionLike);
if ($originalType instanceof MixedType) {
return new MixedType();
}

$type = $this->typeNormalizer->normalizeArrayTypeAndArrayNever($originalType);

// in case of void, check return type of children methods
if ($type instanceof MixedType) {
return new MixedType();
}

$type = $this->verifyStaticType($type, $isSupportedStaticReturnType);
if (! $type instanceof Type) {
return new MixedType();
}

$type = $this->verifyThisType($type, $functionLike);

// normalize ConstStringType to ClassStringType
$resolvedType = $this->genericClassStringTypeNormalizer->normalize($type);
return $this->resolveTypeWithVoidHandling($functionLike, $resolvedType);
}

private function verifyStaticType(Type $type, bool $isSupportedStaticReturnType): ?Type
{
if ($this->isStaticType($type)) {
/** @var TypeWithClassName $type */
return $this->resolveStaticType($isSupportedStaticReturnType, $type);
}

if ($type instanceof UnionType) {
return $this->resolveUnionStaticTypes($type, $isSupportedStaticReturnType);
}

return $type;
}

private function verifyThisType(Type $type, FunctionLike $functionLike): Type
{
if (! $type instanceof ThisType) {
return $type;
}

$classReflection = $this->reflectionResolver->resolveClassReflection($functionLike);
$objectType = $type->getStaticObjectType();
$objectTypeClassName = $objectType->getClassName();

if (! $classReflection instanceof ClassReflection || ! $classReflection->isClass()) {
return $type;
}

if ($classReflection->getName() === $objectTypeClassName) {
return $type;
}

return new MixedType();
}

private function resolveTypeWithVoidHandling(
ClassMethod|Function_|Closure $functionLike,
Type $resolvedType
): Type {
if ($resolvedType->isVoid()->yes()) {
$hasReturnValue = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped(
$functionLike,
static function (Node $subNode): bool {
if (! $subNode instanceof Return_) {
// yield return is handled on speicific rule: AddReturnTypeDeclarationFromYieldsRector
return $subNode instanceof Yield_;
}

return $subNode->expr instanceof Expr;
}
);

if ($hasReturnValue) {
return new MixedType();
}
}

if ($resolvedType instanceof UnionType) {
$benevolentUnionTypeIntegerType = $this->resolveBenevolentUnionTypeInteger($functionLike, $resolvedType);
if ($benevolentUnionTypeIntegerType->isInteger()->yes()) {
return $benevolentUnionTypeIntegerType;
}
}

return $resolvedType;
}

private function resolveBenevolentUnionTypeInteger(
ClassMethod|Function_|Closure $functionLike,
UnionType $unionType
): Type {
$types = $unionType->getTypes();
$countTypes = count($types);

if ($countTypes !== 2) {
return $unionType;
}

if (! ($types[0]->isInteger()->yes() && $types[1]->isString()->yes())) {
return $unionType;
}

$returns = $this->betterNodeFinder->findReturnsScoped($functionLike);
$returnsWithExpr = array_filter(
$returns,
static fn (Return_ $return): bool => $return->expr instanceof Expr
);

if ($returns !== $returnsWithExpr) {
return $unionType;
}

if ($returnsWithExpr === []) {
return $unionType;
}

foreach ($returnsWithExpr as $returnWithExpr) {
/** @var Expr $expr */
$expr = $returnWithExpr->expr;
$type = $this->nodeTypeResolver->getNativeType($expr);

if (! $type instanceof BenevolentUnionType) {
return $unionType;
}
}

return $types[0];
}

private function isStaticType(Type $type): bool
{
if (! $type instanceof TypeWithClassName) {
return false;
}

return $type->getClassName() === ObjectReference::STATIC;
}

private function resolveUnionStaticTypes(UnionType $unionType, bool $isSupportedStaticReturnType): UnionType|null
{
$resolvedTypes = [];
$hasStatic = false;

foreach ($unionType->getTypes() as $unionedType) {
if ($this->isStaticType($unionedType)) {
/** @var FullyQualifiedObjectType $unionedType */
$classReflection = $this->reflectionProvider->getClass($unionedType->getClassName());

$resolvedTypes[] = new ThisType($classReflection);
$hasStatic = true;
continue;
}

$resolvedTypes[] = $unionedType;
}

if (! $hasStatic) {
return $unionType;
}

// has static, but it is not supported
if (! $isSupportedStaticReturnType) {
return null;
}

return new UnionType($resolvedTypes);
}

private function resolveStaticType(
bool $isSupportedStaticReturnType,
TypeWithClassName $typeWithClassName
): ?ThisType {
if (! $isSupportedStaticReturnType) {
return null;
}

$classReflection = $typeWithClassName->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
throw new ShouldNotHappenException();
}

return new ThisType($classReflection);
return $this->typeNormalizer->normalizeArrayTypeAndArrayNever($originalType);
}
}
Loading

0 comments on commit 03ea9b2

Please sign in to comment.