From 8368644b03b97c849a40b113d95fc314232e995b Mon Sep 17 00:00:00 2001 From: Lctrs Date: Wed, 15 Jan 2020 14:24:18 +0100 Subject: [PATCH] Add support for invokable and array callables in CallReflectionResolver --- .../Fixture/array_callable.php.inc | 51 +++++++ .../Fixture/invokable.php.inc | 34 +++++ .../Fixture/stringy_calls.php.inc | 5 + .../Reflection/CallReflectionResolver.php | 129 ++++++++++++++++-- 4 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/array_callable.php.inc create mode 100644 packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/invokable.php.inc diff --git a/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/array_callable.php.inc b/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/array_callable.php.inc new file mode 100644 index 000000000000..9f630846bc58 --- /dev/null +++ b/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/array_callable.php.inc @@ -0,0 +1,51 @@ + +----- + diff --git a/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/invokable.php.inc b/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/invokable.php.inc new file mode 100644 index 000000000000..22803b79a6c4 --- /dev/null +++ b/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/invokable.php.inc @@ -0,0 +1,34 @@ + +----- + diff --git a/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/stringy_calls.php.inc b/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/stringy_calls.php.inc index f526fc73e319..8aeb54dcae76 100644 --- a/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/stringy_calls.php.inc +++ b/packages/Php70/tests/Rector/FuncCall/NonVariableToVariableOnFunctionCallRector/Fixture/stringy_calls.php.inc @@ -9,6 +9,8 @@ class MyClass function stringyCalls() { + 'reset'(bar()); + $functionName = 'reset'; $functionName(bar()); @@ -29,6 +31,9 @@ class MyClass function stringyCalls() { + $bar = bar(); + 'reset'($bar); + $functionName = 'reset'; $bar = bar(); $functionName($bar); diff --git a/src/PHPStan/Reflection/CallReflectionResolver.php b/src/PHPStan/Reflection/CallReflectionResolver.php index 23b7c5b58a08..0437a7cc1272 100644 --- a/src/PHPStan/Reflection/CallReflectionResolver.php +++ b/src/PHPStan/Reflection/CallReflectionResolver.php @@ -19,7 +19,12 @@ 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\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\NodeTypeResolver; use Rector\PhpParser\Node\Resolver\NameResolver; @@ -95,16 +100,24 @@ public function resolveFunctionCall(FuncCall $funcCall) $type = $scope->getType($funcCall->name); - if (! $type instanceof ConstantStringType) { - return new NativeFunctionReflection( - '{closure}', - $type->getCallableParametersAcceptors($scope), - null, - TrinaryLogic::createMaybe() - ); + if ($type instanceof ObjectType) { + return $this->resolveInvokable($type); } - return $this->resolveConstantString($type, $scope); + 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() + ); } /** @@ -161,6 +174,27 @@ public function resolveParametersAcceptor($reflection, Node $node): ?ParametersA } /** + * @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) @@ -190,4 +224,83 @@ private function resolveConstantString(ConstantStringType $constantStringType, S 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; + } + + if ($keyTypes[1]->isSuperTypeOf(new ConstantIntegerType(1))->no()) { + return false; + } + + return true; + } }