Decouple PHPStan Type to function resolver logic

This commit is contained in:
Lctrs 2020-02-10 14:00:23 +01:00
parent 1abb9835e5
commit 11c0655718
7 changed files with 338 additions and 165 deletions

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Rector\Core\PHPStan\Reflection;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
@ -14,32 +13,16 @@ use PHPStan\Analyser\Scope;
use PHPStan\Broker\FunctionNotFoundException;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Native\NativeFunctionReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeAndMethod;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use Rector\Core\PHPStan\Reflection\TypeToCallReflectionResolver\TypeToCallReflectionResolverRegistry;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\NodeTypeResolver;
final class CallReflectionResolver
{
/**
* Took from https://github.com/phpstan/phpstan-src/blob/8376548f76e2c845ae047e3010e873015b796818/src/Type/Constant/ConstantStringType.php#L158
*
* @see https://regex101.com/r/IE6lcM/4
*
* @var string
*/
private const STATIC_METHOD_REGEXP = '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\\z#';
/**
* @var ReflectionProvider
*/
@ -55,14 +38,21 @@ final class CallReflectionResolver
*/
private $nodeNameResolver;
/**
* @var TypeToCallReflectionResolverRegistry
*/
private $typeToCallReflectionResolverRegistry;
public function __construct(
ReflectionProvider $reflectionProvider,
NodeTypeResolver $nodeTypeResolver,
NodeNameResolver $nodeNameResolver
NodeNameResolver $nodeNameResolver,
TypeToCallReflectionResolverRegistry $typeToCallReflectionResolverRegistry
) {
$this->reflectionProvider = $reflectionProvider;
$this->nodeTypeResolver = $nodeTypeResolver;
$this->nodeNameResolver = $nodeNameResolver;
$this->typeToCallReflectionResolverRegistry = $typeToCallReflectionResolverRegistry;
}
/**
@ -98,26 +88,7 @@ final class CallReflectionResolver
return null;
}
$type = $scope->getType($funcCall->name);
if ($type instanceof ObjectType) {
return $this->resolveInvokable($type);
}
if ($type instanceof ConstantStringType) {
return $this->resolveConstantString($type, $scope);
}
if ($type instanceof ConstantArrayType) {
return $this->resolveConstantArray($type, $scope);
}
return new NativeFunctionReflection(
'{closure}',
$type->getCallableParametersAcceptors($scope),
null,
TrinaryLogic::createMaybe()
);
return $this->typeToCallReflectionResolverRegistry->resolve($scope->getType($funcCall->name), $scope);
}
/**
@ -170,130 +141,4 @@ final class CallReflectionResolver
return $parametersAcceptor;
}
/**
* @see https://github.com/phpstan/phpstan-src/blob/b1fd47bda2a7a7d25091197b125c0adf82af6757/src/Type/ObjectType.php#L705
*/
private function resolveInvokable(ObjectType $objectType): ?MethodReflection
{
$className = $objectType->getClassName();
if (! $this->reflectionProvider->hasClass($className)) {
return null;
}
$classReflection = $this->reflectionProvider->getClass($className);
if (! $classReflection->hasNativeMethod('__invoke')) {
return null;
}
return $classReflection->getNativeMethod('__invoke');
}
/**
* @see https://github.com/phpstan/phpstan-src/blob/b1fd47bda2a7a7d25091197b125c0adf82af6757/src/Type/Constant/ConstantStringType.php#L147
*
* @return FunctionReflection|MethodReflection|null
*/
private function resolveConstantString(ConstantStringType $constantStringType, Scope $scope)
{
$value = $constantStringType->getValue();
// 'my_function'
$functionName = new Name($value);
if ($this->reflectionProvider->hasFunction($functionName, null)) {
return $this->reflectionProvider->getFunction($functionName, null);
}
// 'MyClass::myStaticFunction'
$matches = Strings::match($value, self::STATIC_METHOD_REGEXP);
if ($matches === null) {
return null;
}
if (! $this->reflectionProvider->hasClass($matches[1])) {
return null;
}
$classReflection = $this->reflectionProvider->getClass($matches[1]);
if (! $classReflection->hasMethod($matches[2])) {
return null;
}
return $classReflection->getMethod($matches[2], $scope);
}
/**
* @see https://github.com/phpstan/phpstan-src/blob/b1fd47bda2a7a7d25091197b125c0adf82af6757/src/Type/Constant/ConstantArrayType.php#L188
*/
private function resolveConstantArray(ConstantArrayType $constantArrayType, Scope $scope): ?MethodReflection
{
$typeAndMethodName = $this->findTypeAndMethodName($constantArrayType);
if ($typeAndMethodName === null) {
return null;
}
if ($typeAndMethodName->isUnknown() || ! $typeAndMethodName->getCertainty()->yes()) {
return null;
}
$method = $typeAndMethodName
->getType()
->getMethod($typeAndMethodName->getMethod(), $scope);
if (! $scope->canCallMethod($method)) {
return null;
}
return $method;
}
/**
* @see https://github.com/phpstan/phpstan-src/blob/b1fd47bda2a7a7d25091197b125c0adf82af6757/src/Type/Constant/ConstantArrayType.php#L209
*/
private function findTypeAndMethodName(ConstantArrayType $constantArrayType): ?ConstantArrayTypeAndMethod
{
if (! $this->areKeyTypesValid($constantArrayType)) {
return null;
}
[$classOrObject, $method] = $constantArrayType->getValueTypes();
if (! $method instanceof ConstantStringType) {
return ConstantArrayTypeAndMethod::createUnknown();
}
if ($classOrObject instanceof ConstantStringType) {
if (! $this->reflectionProvider->hasClass($classOrObject->getValue())) {
return ConstantArrayTypeAndMethod::createUnknown();
}
$type = new ObjectType($this->reflectionProvider->getClass($classOrObject->getValue())->getName());
} elseif ((new ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) {
$type = $classOrObject;
} else {
return ConstantArrayTypeAndMethod::createUnknown();
}
$has = $type->hasMethod($method->getValue());
if (! $has->no()) {
return ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has);
}
return null;
}
private function areKeyTypesValid(ConstantArrayType $constantArrayType): bool
{
$keyTypes = $constantArrayType->getKeyTypes();
if (count($keyTypes) !== 2) {
return false;
}
if ($keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) {
return false;
}
return ! $keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no();
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Rector\Core\PHPStan\Reflection\TypeToCallReflectionResolver;
use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Reflection\Native\NativeFunctionReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Type;
final class ClosureTypeToCallReflectionResolver implements TypeToCallReflectionResolverInterface
{
public function supports(Type $type): bool
{
return $type instanceof ClosureType;
}
/**
* @param ClosureType $type
*/
public function resolve(Type $type, ClassMemberAccessAnswerer $classMemberAccessAnswerer)
{
return new NativeFunctionReflection(
'{closure}',
$type->getCallableParametersAcceptors($classMemberAccessAnswerer),
null,
TrinaryLogic::createMaybe()
);
}
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Rector\Core\PHPStan\Reflection\TypeToCallReflectionResolver;
use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeAndMethod;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
/**
* @see https://github.com/phpstan/phpstan-src/blob/b1fd47bda2a7a7d25091197b125c0adf82af6757/src/Type/Constant/ConstantArrayType.php#L188
*/
final class ConstantArrayTypeToCallReflectionResolver implements TypeToCallReflectionResolverInterface
{
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
public function __construct(ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}
public function supports(Type $type): bool
{
return $type instanceof ConstantArrayType;
}
/**
* @param ConstantArrayType $type
*/
public function resolve(Type $type, ClassMemberAccessAnswerer $classMemberAccessAnswerer)
{
$typeAndMethodName = $this->findTypeAndMethodName($type);
if ($typeAndMethodName === null) {
return null;
}
if ($typeAndMethodName->isUnknown() || ! $typeAndMethodName->getCertainty()->yes()) {
return null;
}
$method = $typeAndMethodName
->getType()
->getMethod($typeAndMethodName->getMethod(), $classMemberAccessAnswerer);
if (! $classMemberAccessAnswerer->canCallMethod($method)) {
return null;
}
return $method;
}
/**
* @see https://github.com/phpstan/phpstan-src/blob/b1fd47bda2a7a7d25091197b125c0adf82af6757/src/Type/Constant/ConstantArrayType.php#L209
*/
private function findTypeAndMethodName(ConstantArrayType $constantArrayType): ?ConstantArrayTypeAndMethod
{
if (! $this->areKeyTypesValid($constantArrayType)) {
return null;
}
[$classOrObject, $method] = $constantArrayType->getValueTypes();
if (! $method instanceof ConstantStringType) {
return ConstantArrayTypeAndMethod::createUnknown();
}
if ($classOrObject instanceof ConstantStringType) {
if (! $this->reflectionProvider->hasClass($classOrObject->getValue())) {
return ConstantArrayTypeAndMethod::createUnknown();
}
$type = new ObjectType($this->reflectionProvider->getClass($classOrObject->getValue())->getName());
} elseif ((new ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) {
$type = $classOrObject;
} else {
return ConstantArrayTypeAndMethod::createUnknown();
}
$has = $type->hasMethod($method->getValue());
if (! $has->no()) {
return ConstantArrayTypeAndMethod::createConcrete($type, $method->getValue(), $has);
}
return null;
}
private function areKeyTypesValid(ConstantArrayType $constantArrayType): bool
{
$keyTypes = $constantArrayType->getKeyTypes();
if (count($keyTypes) !== 2) {
return false;
}
if ($keyTypes[0]->isSuperTypeOf(new ConstantIntegerType(0))->no()) {
return false;
}
return ! $keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no();
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Rector\Core\PHPStan\Reflection\TypeToCallReflectionResolver;
use Nette\Utils\Strings;
use PhpParser\Node\Name;
use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Type;
/**
* @see https://github.com/phpstan/phpstan-src/blob/b1fd47bda2a7a7d25091197b125c0adf82af6757/src/Type/Constant/ConstantStringType.php#L147
*/
final class ConstantStringTypeToCallReflectionResolver implements TypeToCallReflectionResolverInterface
{
/**
* Took from https://github.com/phpstan/phpstan-src/blob/8376548f76e2c845ae047e3010e873015b796818/src/Type/Constant/ConstantStringType.php#L158
*
* @see https://regex101.com/r/IE6lcM/4
*
* @var string
*/
private const STATIC_METHOD_REGEXP = '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\\z#';
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
public function __construct(ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}
public function supports(Type $type): bool
{
return $type instanceof ConstantStringType;
}
/**
* @param ConstantStringType $type
*/
public function resolve(Type $type, ClassMemberAccessAnswerer $classMemberAccessAnswerer)
{
$value = $type->getValue();
// 'my_function'
$functionName = new Name($value);
if ($this->reflectionProvider->hasFunction($functionName, null)) {
return $this->reflectionProvider->getFunction($functionName, null);
}
// 'MyClass::myStaticFunction'
$matches = Strings::match($value, self::STATIC_METHOD_REGEXP);
if ($matches === null) {
return null;
}
if (! $this->reflectionProvider->hasClass($matches[1])) {
return null;
}
$classReflection = $this->reflectionProvider->getClass($matches[1]);
if (! $classReflection->hasMethod($matches[2])) {
return null;
}
return $classReflection->getMethod($matches[2], $classMemberAccessAnswerer);
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Rector\Core\PHPStan\Reflection\TypeToCallReflectionResolver;
use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
/**
* @see https://github.com/phpstan/phpstan-src/blob/b1fd47bda2a7a7d25091197b125c0adf82af6757/src/Type/ObjectType.php#L705
*/
final class ObjectTypeToCallReflectionResolver implements TypeToCallReflectionResolverInterface
{
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
public function __construct(ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}
public function supports(Type $type): bool
{
return $type instanceof ObjectType;
}
/**
* @param ObjectType $type
*/
public function resolve(Type $type, ClassMemberAccessAnswerer $classMemberAccessAnswerer)
{
$className = $type->getClassName();
if (! $this->reflectionProvider->hasClass($className)) {
return null;
}
$classReflection = $this->reflectionProvider->getClass($className);
if (! $classReflection->hasNativeMethod('__invoke')) {
return null;
}
return $classReflection->getNativeMethod('__invoke');
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Rector\Core\PHPStan\Reflection\TypeToCallReflectionResolver;
use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Type;
interface TypeToCallReflectionResolverInterface
{
public function supports(Type $type): bool;
/**
* @return FunctionReflection|MethodReflection|null
*/
public function resolve(Type $type, ClassMemberAccessAnswerer $classMemberAccessAnswerer);
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Rector\Core\PHPStan\Reflection\TypeToCallReflectionResolver;
use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Type;
final class TypeToCallReflectionResolverRegistry
{
/**
* @var TypeToCallReflectionResolverInterface[]
*/
private $resolvers = [];
/**
* @param TypeToCallReflectionResolverInterface[] $resolvers
*/
public function __construct(array $resolvers)
{
$this->resolvers = $resolvers;
}
/**
* @return FunctionReflection|MethodReflection|null
*/
public function resolve(Type $type, ClassMemberAccessAnswerer $classMemberAccessAnswerer)
{
foreach ($this->resolvers as $resolver) {
if (! $resolver->supports($type)) {
continue;
}
return $resolver->resolve($type, $classMemberAccessAnswerer);
}
return null;
}
}