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