diff --git a/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrowFunctionParamArrayWhereDimFetchRector/Fixture/handle_usort.php.inc b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrowFunctionParamArrayWhereDimFetchRector/Fixture/handle_usort.php.inc new file mode 100644 index 00000000000..8366d44243e --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrowFunctionParamArrayWhereDimFetchRector/Fixture/handle_usort.php.inc @@ -0,0 +1,33 @@ + $a['key'] <=> $b['key'] + ); + } +} + +?> +----- + $a['key'] <=> $b['key'] + ); + } +} + +?> diff --git a/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php b/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php index 8c28029fccb..f3fccaefdb4 100644 --- a/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php +++ b/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php @@ -65,15 +65,19 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { + if ($node->isFirstClassCallable()) { + return null; + } + + if (count($node->getArgs()) !== 2) { + return null; + } + foreach (NativeFuncCallPositions::ARRAY_AND_CALLBACK_POSITIONS as $functionName => $positions) { if (! $this->isName($node, $functionName)) { continue; } - if ($node->isFirstClassCallable()) { - continue; - } - $arrayPosition = $positions['array']; $callbackPosition = $positions['callback']; diff --git a/rules/TypeDeclaration/Rector/FuncCall/AddArrowFunctionParamArrayWhereDimFetchRector.php b/rules/TypeDeclaration/Rector/FuncCall/AddArrowFunctionParamArrayWhereDimFetchRector.php index c24fde46ec7..cbc0019413c 100644 --- a/rules/TypeDeclaration/Rector/FuncCall/AddArrowFunctionParamArrayWhereDimFetchRector.php +++ b/rules/TypeDeclaration/Rector/FuncCall/AddArrowFunctionParamArrayWhereDimFetchRector.php @@ -7,9 +7,12 @@ use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\ArrowFunction; +use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Identifier; +use Rector\PhpParser\Node\BetterNodeFinder; use Rector\Rector\AbstractRector; +use Rector\TypeDeclaration\Enum\NativeFuncCallPositions; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -20,6 +23,11 @@ */ final class AddArrowFunctionParamArrayWhereDimFetchRector extends AbstractRector implements MinPhpVersionInterface { + public function __construct( + private BetterNodeFinder $betterNodeFinder + ) { + } + public function getRuleDefinition(): RuleDefinition { return new RuleDefinition('Add function/closure param array type, if dim fetch is inside', [ @@ -53,43 +61,58 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if (! $this->isName($node, 'array_map')) { - return null; - } - if ($node->isFirstClassCallable()) { return null; } - $firstArgExpr = $node->getArgs()[0] - ->value; - if (! $firstArgExpr instanceof ArrowFunction) { + if (count($node->getArgs()) !== 2) { return null; } - $arrowFunction = $firstArgExpr; - $arrowFunctionParam = $arrowFunction->getParams()[0]; + $hasChanged = false; - // param is known already - if ($arrowFunctionParam->type instanceof Node) { - return null; - } + foreach (NativeFuncCallPositions::ARRAY_AND_CALLBACK_POSITIONS as $functionName => $positions) { + if (! $this->isName($node, $functionName)) { + continue; + } - if (! $arrowFunction->expr instanceof ArrayDimFetch) { - return null; - } + $callbackPosition = $positions['callback']; + + $closureExpr = $node->getArgs()[$callbackPosition]->value; + if (! $closureExpr instanceof ArrowFunction && ! $closureExpr instanceof Closure) { + continue; + } + + $isArrayVariableNames = $this->resolveIsArrayVariables($closureExpr); + $instanceofVariableNames = $this->resolveInstanceofVariables($closureExpr); + $skippedVariableNames = array_merge($isArrayVariableNames, $instanceofVariableNames); + + $dimFetchVariableNames = $this->resolveDimFetchVariableNames($closureExpr); + + foreach ($closureExpr->getParams() as $closureParam) { + if ($closureParam->type instanceof \PhpParser\Node) { + // param is known already + continue; + } - $var = $arrowFunction->expr; - while ($var instanceof ArrayDimFetch) { - $var = $var->var; + // skip is_array() checked variables + if ($this->isNames($closureParam->var, $skippedVariableNames)) { + continue; + } + + if (! $this->isNames($closureParam->var, $dimFetchVariableNames)) { + continue; + } + + $hasChanged = true; + $closureParam->type = new Identifier('array'); + } } - if (! $this->nodeComparator->areNodesEqual($var, $arrowFunctionParam->var)) { + if ($hasChanged === false) { return null; } - $arrowFunctionParam->type = new Identifier('array'); - return $node; } @@ -97,4 +120,89 @@ public function provideMinPhpVersion(): int { return PhpVersionFeature::SCALAR_TYPES; } + + /** + * @return string[] + */ + private function resolveDimFetchVariableNames(Closure|ArrowFunction $closureExpr): array + { + if ($closureExpr instanceof ArrowFunction) { + $closureNodes = [$closureExpr->expr]; + } else { + $closureNodes = $closureExpr->stmts; + } + + /** @var ArrayDimFetch[] $arrayDimFetches */ + $arrayDimFetches = $this->betterNodeFinder->findInstancesOfScoped($closureNodes, ArrayDimFetch::class); + + $usedDimFetchVariableNames = []; + + foreach ($arrayDimFetches as $arrayDimFetch) { + if ($arrayDimFetch->var instanceof Node\Expr\Variable) { + $usedDimFetchVariableNames[] = (string) $this->getName($arrayDimFetch->var); + } + } + + return $usedDimFetchVariableNames; + } + + /** + * @return string[] + */ + private function resolveIsArrayVariables(Closure|ArrowFunction $closureExpr): array + { + if ($closureExpr instanceof ArrowFunction) { + $closureNodes = [$closureExpr->expr]; + } else { + $closureNodes = $closureExpr->stmts; + } + + /** @var FuncCall[] $funcCalls */ + $funcCalls = $this->betterNodeFinder->findInstancesOfScoped($closureNodes, FuncCall::class); + + $variableNames = []; + + foreach ($funcCalls as $funcCall) { + if (! $this->isName($funcCall, 'is_array')) { + continue; + } + + $firstArgExpr = $funcCall->getArgs()[0] + ->value; + if (! $firstArgExpr instanceof Node\Expr\Variable) { + continue; + } + + $variableNames[] = (string) $this->getName($firstArgExpr); + } + + return $variableNames; + } + + /** + * @return string[] + */ + private function resolveInstanceofVariables(Closure|ArrowFunction $closureExpr): array + { + if ($closureExpr instanceof ArrowFunction) { + $closureNodes = [$closureExpr->expr]; + } else { + $closureNodes = $closureExpr->stmts; + } + + /** @var Node\Expr\Instanceof_[] $instanceOfs */ + $instanceOfs = $this->betterNodeFinder->findInstancesOfScoped($closureNodes, Node\Expr\Instanceof_::class); + + $variableNames = []; + + foreach ($instanceOfs as $instanceOf) { + if (! $instanceOf->expr instanceof Node\Expr\Variable) { + continue; + } + + $variableNames[] = (string) $this->getName($instanceOf->expr); + } + + return $variableNames; + } } diff --git a/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php index 4f0a5a3e2ea..eb36169dd42 100644 --- a/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php +++ b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php @@ -4,12 +4,12 @@ namespace Rector\TypeDeclaration\Rector\FunctionLike; -use PhpParser\Node\VariadicPlaceholder; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Param; +use PhpParser\Node\VariadicPlaceholder; use PHPStan\Reflection\Native\NativeFunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\MixedType;