rector/packages/PHPStanStaticTypeMapper/TypeAnalyzer/UnionTypeCommonTypeNarrower...

166 lines
6.8 KiB
PHP

<?php
declare (strict_types=1);
namespace Rector\PHPStanStaticTypeMapper\TypeAnalyzer;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\ClassLike;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\UnionType;
use Rector\Core\Contract\Rector\RectorInterface;
use Rector\NodeTypeResolver\NodeTypeCorrector\GenericClassStringTypeCorrector;
final class UnionTypeCommonTypeNarrower
{
/**
* @readonly
* @var \Rector\NodeTypeResolver\NodeTypeCorrector\GenericClassStringTypeCorrector
*/
private $genericClassStringTypeCorrector;
/**
* @readonly
* @var \PHPStan\Reflection\ReflectionProvider
*/
private $reflectionProvider;
/**
* Key = the winner Array = the group of types matched
*
* @var array<string, array<class-string<Node>|class-string<\PHPStan\PhpDocParser\Ast\Node>|class-string<RectorInterface>>>
*/
private const PRIORITY_TYPES = [ClassLike::class => [ClassLike::class], FunctionLike::class => [FunctionLike::class], BinaryOp::class => [BinaryOp::class, Expr::class], Expr::class => [Node::class, Expr::class], Stmt::class => [Node::class, Stmt::class], PhpDocTagValueNode::class => [PhpDocTagValueNode::class, \PHPStan\PhpDocParser\Ast\Node::class], Node::class => [Node::class], RectorInterface::class => [RectorInterface::class]];
public function __construct(GenericClassStringTypeCorrector $genericClassStringTypeCorrector, ReflectionProvider $reflectionProvider)
{
$this->genericClassStringTypeCorrector = $genericClassStringTypeCorrector;
$this->reflectionProvider = $reflectionProvider;
}
public function narrowToSharedObjectType(UnionType $unionType) : ?ObjectType
{
$sharedTypes = $this->narrowToSharedTypes($unionType);
if ($sharedTypes !== []) {
foreach (self::PRIORITY_TYPES as $winningType => $groupTypes) {
$intersectedGroupTypes = \array_intersect($groupTypes, $sharedTypes);
if ($intersectedGroupTypes === $groupTypes) {
return new ObjectType($winningType);
}
}
$firstSharedType = $sharedTypes[0];
return new ObjectType($firstSharedType);
}
return null;
}
/**
* @return \PHPStan\Type\UnionType|\PHPStan\Type\Generic\GenericClassStringType
*/
public function narrowToGenericClassStringType(UnionType $unionType)
{
$availableTypes = [];
foreach ($unionType->getTypes() as $unionedType) {
if ($unionedType instanceof ConstantStringType) {
$unionedType = $this->genericClassStringTypeCorrector->correct($unionedType);
}
if (!$unionedType instanceof GenericClassStringType) {
return $unionType;
}
$genericClassStrings = [];
if ($unionedType->getGenericType() instanceof ObjectType) {
$parentClassReflections = $this->resolveClassParentClassesAndInterfaces($unionedType->getGenericType());
foreach ($parentClassReflections as $parentClassReflection) {
$genericClassStrings[] = $parentClassReflection->getName();
}
}
$availableTypes[] = $genericClassStrings;
}
$genericClassStringType = $this->createGenericClassStringType($availableTypes);
if ($genericClassStringType instanceof GenericClassStringType) {
return $genericClassStringType;
}
return $unionType;
}
/**
* @return string[]
*/
private function narrowToSharedTypes(UnionType $unionType) : array
{
$availableTypes = [];
foreach ($unionType->getTypes() as $unionedType) {
if (!$unionedType instanceof ObjectType) {
return [];
}
$typeClassReflections = $this->resolveClassParentClassesAndInterfaces($unionedType);
$typeClassNames = [];
foreach ($typeClassReflections as $typeClassReflection) {
$typeClassNames[] = $typeClassReflection->getName();
}
if ($typeClassNames === []) {
continue;
}
$availableTypes[] = $typeClassNames;
}
return $this->narrowAvailableTypes($availableTypes);
}
/**
* @return ClassReflection[]
*/
private function resolveClassParentClassesAndInterfaces(ObjectType $objectType) : array
{
if (!$this->reflectionProvider->hasClass($objectType->getClassName())) {
return [];
}
$classReflection = $this->reflectionProvider->getClass($objectType->getClassName());
// put earliest interfaces first
$implementedInterfaceClassReflections = \array_reverse($classReflection->getInterfaces());
/** @var ClassReflection[] $parentClassAndInterfaceReflections */
$parentClassAndInterfaceReflections = \array_merge($implementedInterfaceClassReflections, $classReflection->getParents());
return $this->filterOutNativeClassReflections($parentClassAndInterfaceReflections);
}
/**
* @param string[][] $availableTypes
* @return string[]
*/
private function narrowAvailableTypes(array $availableTypes) : array
{
if (\count($availableTypes) < 2) {
return [];
}
/** @var string[] $sharedTypes */
$sharedTypes = \array_intersect(...$availableTypes);
return \array_values($sharedTypes);
}
/**
* @param string[][] $availableTypes
*/
private function createGenericClassStringType(array $availableTypes) : ?GenericClassStringType
{
$sharedTypes = $this->narrowAvailableTypes($availableTypes);
if ($sharedTypes !== []) {
foreach (self::PRIORITY_TYPES as $winningType => $groupTypes) {
$intersectedGroupTypes = \array_intersect($groupTypes, $sharedTypes);
if ($intersectedGroupTypes === $groupTypes) {
return new GenericClassStringType(new ObjectType($winningType));
}
}
$firstSharedType = $sharedTypes[0];
return new GenericClassStringType(new ObjectType($firstSharedType));
}
return null;
}
/**
* @param ClassReflection[] $classReflections
* @return ClassReflection[]
*/
private function filterOutNativeClassReflections(array $classReflections) : array
{
return \array_filter($classReflections, static function (ClassReflection $classReflection) : bool {
return !$classReflection->isBuiltin();
});
}
}