diff --git a/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/AddArrayFunctionClosureParamTypeRectorTest.php b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/AddArrayFunctionClosureParamTypeRectorTest.php new file mode 100644 index 00000000000..a1da32cafd6 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/AddArrayFunctionClosureParamTypeRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/array_filter_with_docblock_type.php.inc b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/array_filter_with_docblock_type.php.inc new file mode 100644 index 00000000000..bd0703b44f2 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/array_filter_with_docblock_type.php.inc @@ -0,0 +1,33 @@ + $item * 2); + } +} + +?> +----- + $item * 2); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/include_array_map.php.inc b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/include_array_map.php.inc new file mode 100644 index 00000000000..33cebffe87c --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/include_array_map.php.inc @@ -0,0 +1,33 @@ + $item * 2, $items); + } +} + +?> +----- + $item * 2, $items); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/skip_unclear_param.php.inc b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/skip_unclear_param.php.inc new file mode 100644 index 00000000000..a382cf1337d --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/skip_unclear_param.php.inc @@ -0,0 +1,14 @@ + $item * 2); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/some_function.php.inc b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/some_function.php.inc new file mode 100644 index 00000000000..286cd63bd86 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/some_function.php.inc @@ -0,0 +1,29 @@ + $item * 2); + } +} + +?> +----- + $item * 2); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/config/configured_rule.php b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/config/configured_rule.php new file mode 100644 index 00000000000..2f210e0509c --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([AddArrayFunctionClosureParamTypeRector::class]); diff --git a/rules/TypeDeclaration/Enum/NativeFuncCallPositions.php b/rules/TypeDeclaration/Enum/NativeFuncCallPositions.php new file mode 100644 index 00000000000..fe17bd9c1b4 --- /dev/null +++ b/rules/TypeDeclaration/Enum/NativeFuncCallPositions.php @@ -0,0 +1,30 @@ +> + */ + public const ARRAY_AND_CALLBACK_POSITIONS = [ + 'array_walk' => [ + 'array' => 0, + 'callback' => 1, + ], + 'array_map' => [ + 'array' => 1, + 'callback' => 0, + ], + 'usort' => [ + 'array' => 0, + 'callback' => 1, + ], + 'array_filter' => [ + 'array' => 0, + 'callback' => 1, + ], + ]; +} diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddParamArrayDocblockBasedOnCallableNativeFuncCallRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddParamArrayDocblockBasedOnCallableNativeFuncCallRector.php index 681bb49e032..555208554d1 100644 --- a/rules/TypeDeclaration/Rector/ClassMethod/AddParamArrayDocblockBasedOnCallableNativeFuncCallRector.php +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddParamArrayDocblockBasedOnCallableNativeFuncCallRector.php @@ -25,6 +25,7 @@ use Rector\NodeAnalyzer\ArgsAnalyzer; use Rector\Rector\AbstractRector; use Rector\StaticTypeMapper\StaticTypeMapper; +use Rector\TypeDeclaration\Enum\NativeFuncCallPositions; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -33,28 +34,6 @@ */ final class AddParamArrayDocblockBasedOnCallableNativeFuncCallRector extends AbstractRector { - /** - * @var array> - */ - private const NATIVE_FUNC_CALLS_WITH_POSITION = [ - 'array_walk' => [ - 'array' => 0, - 'callback' => 1, - ], - 'array_map' => [ - 'array' => 1, - 'callback' => 0, - ], - 'usort' => [ - 'array' => 0, - 'callback' => 1, - ], - 'array_filter' => [ - 'array' => 0, - 'callback' => 1, - ], - ]; - public function __construct( private readonly PhpDocInfoFactory $phpDocInfoFactory, private readonly ArgsAnalyzer $argsAnalyzer, @@ -70,8 +49,8 @@ public function getRuleDefinition(): RuleDefinition <<<'CODE_SAMPLE' function process(array $items): void { - array_walk($items, function (stdClass $item) { - echo $item->value; + array_walk($items, function (stdClass $item) { + echo $item->value; }); } CODE_SAMPLE @@ -82,8 +61,8 @@ function process(array $items): void */ function process(array $items): void { - array_walk($items, function (stdClass $item) { - echo $item->value; + array_walk($items, function (stdClass $item) { + echo $item->value; }); } CODE_SAMPLE @@ -131,7 +110,7 @@ function (Node $subNode) use ($variableNamesWithArrayType, $node, &$paramsWithTy return null; } - if (! $this->isNames($subNode, array_keys(self::NATIVE_FUNC_CALLS_WITH_POSITION))) { + if (! $this->isNames($subNode, array_keys(NativeFuncCallPositions::ARRAY_AND_CALLBACK_POSITIONS))) { return null; } @@ -150,7 +129,7 @@ function (Node $subNode) use ($variableNamesWithArrayType, $node, &$paramsWithTy $funcCallName = (string) $this->getName($subNode); - $arrayArgValue = $args[self::NATIVE_FUNC_CALLS_WITH_POSITION[$funcCallName]['array']]->value; + $arrayArgValue = $args[NativeFuncCallPositions::ARRAY_AND_CALLBACK_POSITIONS[$funcCallName]['array']]->value; if (! $arrayArgValue instanceof Variable) { return null; } @@ -167,7 +146,7 @@ function (Node $subNode) use ($variableNamesWithArrayType, $node, &$paramsWithTy return null; } - $callbackArgValue = $args[self::NATIVE_FUNC_CALLS_WITH_POSITION[$funcCallName]['callback']]->value; + $callbackArgValue = $args[NativeFuncCallPositions::ARRAY_AND_CALLBACK_POSITIONS[$funcCallName]['callback']]->value; if (! $callbackArgValue instanceof ArrowFunction && ! $callbackArgValue instanceof Closure) { return null; diff --git a/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php b/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php new file mode 100644 index 00000000000..8c28029fccb --- /dev/null +++ b/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php @@ -0,0 +1,126 @@ + $item > 1); +CODE_SAMPLE + + , + <<<'CODE_SAMPLE' +$items = [1, 2, 3]; +$result = array_filter($items, fn (int $item) => $item > 1 +CODE_SAMPLE + ), + + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [FuncCall::class]; + } + + /** + * @param FuncCall $node + */ + public function refactor(Node $node): ?Node + { + 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']; + + $firstArgExpr = $node->getArgs()[$callbackPosition] + ->value; + if (! $firstArgExpr instanceof ArrowFunction && ! $firstArgExpr instanceof Closure) { + continue; + } + + $arrowFunction = $firstArgExpr; + $arrowFunctionParam = $arrowFunction->getParams()[0]; + + // param is known already + if ($arrowFunctionParam->type instanceof Node) { + continue; + } + + $passedExprType = $this->getType($node->getArgs()[$arrayPosition]->value); + if ($passedExprType instanceof ConstantArrayType || $passedExprType instanceof ArrayType) { + $singlePassedExprType = $passedExprType->getItemType(); + + if ($singlePassedExprType instanceof MixedType) { + continue; + } + + $paramType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $singlePassedExprType, + TypeKind::PARAM + ); + + if (! $paramType instanceof Node) { + continue; + } + + $arrowFunctionParam->type = $paramType; + + return $node; + } + + return null; + } + + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::SCALAR_TYPES; + } +} diff --git a/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php index e6d771820d9..4f0a5a3e2ea 100644 --- a/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php +++ b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php @@ -4,6 +4,7 @@ namespace Rector\TypeDeclaration\Rector\FunctionLike; +use PhpParser\Node\VariadicPlaceholder; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\Closure; @@ -89,7 +90,7 @@ public function refactor(Node $node): ?Node } /** @var ArrayType[] $types */ - $types = array_filter(array_map(function ($arg): ?ArrayType { + $types = array_filter(array_map(function (Arg|VariadicPlaceholder $arg): ?ArrayType { if (! $arg instanceof Arg) { return null; } diff --git a/src/ChangesReporting/Output/GitHubOutputFormatter.php b/src/ChangesReporting/Output/GitHubOutputFormatter.php index 30b6b0496c9..8516157d619 100644 --- a/src/ChangesReporting/Output/GitHubOutputFormatter.php +++ b/src/ChangesReporting/Output/GitHubOutputFormatter.php @@ -125,10 +125,13 @@ private function sanitizeAnnotationProperties(array $annotationProperties): stri // TODO: Should be removed once github will have fixed it issue. unset($annotationProperties['endLine']); - $nonNullProperties = array_filter($annotationProperties, static fn ($value): bool => $value !== null); + $nonNullProperties = array_filter( + $annotationProperties, + static fn (int|string|null $value): bool => $value !== null + ); $sanitizedProperties = array_map( - fn ($key, $value): string => sprintf('%s=%s', $key, $this->sanitizeAnnotationProperty($value)), + fn (string $key, $value): string => sprintf('%s=%s', $key, $this->sanitizeAnnotationProperty($value)), array_keys($nonNullProperties), $nonNullProperties ); diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index a9d34e64c88..1ac9994995b 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -49,6 +49,7 @@ use Rector\TypeDeclaration\Rector\Closure\AddClosureVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeRector; use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector; +use Rector\TypeDeclaration\Rector\FuncCall\AddArrayFunctionClosureParamTypeRector; use Rector\TypeDeclaration\Rector\FuncCall\AddArrowFunctionParamArrayWhereDimFetchRector; use Rector\TypeDeclaration\Rector\Function_\AddFunctionVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\FunctionLike\AddParamTypeSplFixedArrayRector; @@ -138,5 +139,8 @@ final class TypeDeclarationLevel StrictArrayParamDimFetchRector::class, StrictStringParamConcatRector::class, TypedPropertyFromJMSSerializerAttributeTypeRector::class, + + // possibly based on docblocks, but also helpful, intentionally last + AddArrayFunctionClosureParamTypeRector::class, ]; }