rector/rules/code-quality/src/Rector/Array_/CallableThisArrayToAnonymousFunctionRector.php

301 lines
8.0 KiB
PHP
Raw Normal View History

2019-10-13 05:59:52 +00:00
<?php
declare(strict_types=1);
namespace Rector\CodeQuality\Rector\Array_;
use PhpParser\Node;
2019-04-17 19:17:29 +00:00
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
2019-04-17 19:17:29 +00:00
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ClosureUse;
use PhpParser\Node\Expr\FuncCall;
2019-04-17 19:17:29 +00:00
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
2019-04-17 19:17:29 +00:00
use PhpParser\Node\Expr\Variable;
2019-04-30 22:49:59 +00:00
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\String_;
2019-04-17 19:17:29 +00:00
use PhpParser\Node\Stmt\ClassMethod;
2019-05-31 06:52:12 +00:00
use PhpParser\Node\Stmt\Expression;
2019-04-17 19:17:29 +00:00
use PhpParser\Node\Stmt\Return_;
2019-09-04 12:10:29 +00:00
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Rector\AbstractRector;
2019-11-07 20:37:39 +00:00
use Rector\NodeTypeResolver\Node\AttributeKey;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
2019-04-06 17:09:29 +00:00
* @see https://www.php.net/manual/en/language.types.callable.php#117260
* @see https://3v4l.org/MsMbQ
* @see https://3v4l.org/KM1Ji
2019-09-04 12:10:29 +00:00
*
2019-09-03 09:11:45 +00:00
* @see \Rector\CodeQuality\Tests\Rector\Array_\CallableThisArrayToAnonymousFunctionRector\CallableThisArrayToAnonymousFunctionRectorTest
*/
final class CallableThisArrayToAnonymousFunctionRector extends AbstractRector
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Convert [$this, "method"] to proper anonymous function',
[
new CodeSample(
<<<'CODE_SAMPLE'
class SomeClass
{
public function run()
{
$values = [1, 5, 3];
usort($values, [$this, 'compareSize']);
return $values;
}
private function compareSize($first, $second)
{
return $first <=> $second;
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeClass
{
public function run()
{
$values = [1, 5, 3];
usort($values, function ($first, $second) {
return $this->compareSize($first, $second);
});
return $values;
}
private function compareSize($first, $second)
{
return $first <=> $second;
}
}
CODE_SAMPLE
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
2019-04-17 19:17:29 +00:00
return [Array_::class];
}
/**
2019-04-30 22:49:59 +00:00
* @param Array_ $node
*/
public function refactor(Node $node): ?Node
{
if ($this->shouldSkipArray($node)) {
return null;
}
$firstArrayItem = $node->items[0];
if (! $firstArrayItem instanceof ArrayItem) {
return null;
}
$objectVariable = $firstArrayItem->value;
if (! $objectVariable instanceof Variable && ! $objectVariable instanceof PropertyFetch) {
return null;
}
$secondArrayItem = $node->items[1];
if (! $secondArrayItem instanceof ArrayItem) {
return null;
}
$methodName = $secondArrayItem->value;
if (! $methodName instanceof String_) {
return null;
}
$classMethod = $this->matchCallableMethod($objectVariable, $methodName);
if ($classMethod === null) {
return null;
}
return $this->createAnonymousFunction($classMethod, $objectVariable);
}
private function shouldSkipArray(Array_ $array): bool
{
// callback is exactly "[$two, 'items']"
if (count((array) $array->items) !== 2) {
return true;
}
// can be totally empty in case of "[, $value]"
if ($array->items[0] === null) {
return true;
}
if ($array->items[1] === null) {
return true;
}
return $this->isCallbackAtFunctionName($array, 'register_shutdown_function');
}
/**
* @param Variable|PropertyFetch $objectExpr
*/
2020-06-29 21:19:37 +00:00
private function matchCallableMethod(Expr $objectExpr, String_ $string): ?ClassMethod
{
2020-06-29 21:19:37 +00:00
$methodName = $this->getValue($string);
2019-12-30 13:52:18 +00:00
if (! is_string($methodName)) {
throw new ShouldNotHappenException();
}
2019-09-04 12:10:29 +00:00
$objectType = $this->getObjectType($objectExpr);
$objectType = $this->popFirstObjectType($objectType);
2019-09-04 12:10:29 +00:00
if ($objectType instanceof ObjectType) {
$class = $this->nodeRepository->findClass($objectType->getClassName());
if ($class === null) {
2019-09-04 12:10:29 +00:00
return null;
}
$classMethod = $class->getMethod($methodName);
2019-09-04 12:10:29 +00:00
if ($classMethod === null) {
2019-09-04 12:10:29 +00:00
return null;
}
if ($this->isName($objectExpr, 'this')) {
return $classMethod;
}
2019-09-04 12:10:29 +00:00
// is public method of another service
if ($classMethod->isPublic()) {
return $classMethod;
}
}
return null;
}
/**
* @param Variable|PropertyFetch $node
*/
private function createAnonymousFunction(ClassMethod $classMethod, Node $node): Closure
{
$classMethodReturns = $this->betterNodeFinder->findInstanceOf((array) $classMethod->stmts, Return_::class);
$anonymousFunction = new Closure();
2019-11-07 20:37:39 +00:00
$newParams = $this->copyParams($classMethod->params);
$anonymousFunction->params = $newParams;
$innerMethodCall = new MethodCall($node, $classMethod->name);
2019-11-07 20:37:39 +00:00
$innerMethodCall->args = $this->convertParamsToArgs($newParams);
if ($classMethod->returnType !== null) {
2019-11-07 20:37:39 +00:00
$newReturnType = $classMethod->returnType;
$newReturnType->setAttribute(AttributeKey::ORIGINAL_NODE, null);
$anonymousFunction->returnType = $newReturnType;
}
// does method return something?
if ($this->hasClassMethodReturn($classMethodReturns)) {
$anonymousFunction->stmts[] = new Return_($innerMethodCall);
} else {
$anonymousFunction->stmts[] = new Expression($innerMethodCall);
}
if ($node instanceof Variable && ! $this->isName($node, 'this')) {
$anonymousFunction->uses[] = new ClosureUse($node);
}
return $anonymousFunction;
}
2020-02-01 16:04:38 +00:00
private function isCallbackAtFunctionName(Array_ $array, string $functionName): bool
{
2020-02-01 16:04:38 +00:00
$parentNode = $array->getAttribute(AttributeKey::PARENT_NODE);
if (! $parentNode instanceof Arg) {
return false;
}
2020-02-01 16:04:38 +00:00
$parentParentNode = $parentNode->getAttribute(AttributeKey::PARENT_NODE);
if (! $parentParentNode instanceof FuncCall) {
return false;
}
2020-02-01 16:04:38 +00:00
return $this->isName($parentParentNode, $functionName);
}
2019-11-07 20:37:39 +00:00
private function popFirstObjectType(Type $type): Type
{
if ($type instanceof UnionType) {
foreach ($type->getTypes() as $unionedType) {
if (! $unionedType instanceof ObjectType) {
continue;
}
return $unionedType;
}
}
return $type;
}
2019-11-07 20:37:39 +00:00
/**
* @param Param[] $params
* @return Param[]
*/
private function copyParams(array $params): array
{
$newParams = [];
foreach ($params as $param) {
$newParam = clone $param;
$newParam->setAttribute(AttributeKey::ORIGINAL_NODE, null);
$newParam->var->setAttribute(AttributeKey::ORIGINAL_NODE, null);
$newParams[] = $newParam;
}
return $newParams;
}
2020-02-01 16:04:38 +00:00
/**
* @param Param[] $params
* @return Arg[]
*/
private function convertParamsToArgs(array $params): array
{
2020-02-01 16:04:38 +00:00
$args = [];
foreach ($params as $param) {
$args[] = new Arg($param->var);
}
2020-02-01 16:04:38 +00:00
return $args;
}
2020-02-01 16:04:38 +00:00
/**
* @param Return_[] $nodes
*/
private function hasClassMethodReturn(array $nodes): bool
{
foreach ($nodes as $node) {
if ($node->expr !== null) {
return true;
}
}
return false;
}
}