From 9d6fdd235c2f997ed832e945f1bfdada02fb7ed2 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 6 Aug 2025 10:18:36 +0200 Subject: [PATCH 1/3] [type-declaration] Add AddArrayFilterClosureParamTypeRector --- ...rrayFunctionClosureParamTypeRectorTest.php | 28 +++++ .../Fixture/some_function.php.inc | 29 +++++ .../config/configured_rule.php | 9 ++ ...AddArrayFunctionClosureParamTypeRector.php | 105 ++++++++++++++++++ .../Output/GitHubOutputFormatter.php | 2 +- src/Config/Level/TypeDeclarationLevel.php | 4 + 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/AddArrayFunctionClosureParamTypeRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/some_function.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/config/configured_rule.php create mode 100644 rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php 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/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/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php b/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php new file mode 100644 index 00000000000..a1fa9502c5f --- /dev/null +++ b/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php @@ -0,0 +1,105 @@ + $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 + { + if (! $this->isName($node, 'array_filter')) { + return null; + } + + if ($node->isFirstClassCallable()) { + return null; + } + + $firstArgExpr = $node->getArgs()[1] + ->value; + if (! $firstArgExpr instanceof ArrowFunction && ! $firstArgExpr instanceof Closure) { + return null; + } + + $arrowFunction = $firstArgExpr; + $arrowFunctionParam = $arrowFunction->getParams()[0]; + + // param is known already + if ($arrowFunctionParam->type instanceof Node) { + return null; + } + + $passedExprType = $this->getType($node->getArgs()[0]->value); + if ($passedExprType instanceof ConstantArrayType) { + $singlePassedExprType = $passedExprType->getItemType(); + + $paramType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($singlePassedExprType, TypeKind::PARAM); + + if (! $paramType instanceof Node) { + return null; + } + + $arrowFunctionParam->type = $paramType; + + return $node; + } + + return null; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::SCALAR_TYPES; + } +} diff --git a/src/ChangesReporting/Output/GitHubOutputFormatter.php b/src/ChangesReporting/Output/GitHubOutputFormatter.php index 30b6b0496c9..27ea6b3bf45 100644 --- a/src/ChangesReporting/Output/GitHubOutputFormatter.php +++ b/src/ChangesReporting/Output/GitHubOutputFormatter.php @@ -125,7 +125,7 @@ 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)), 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, ]; } From 7c3cc0465851dca75f2129cf1219552444289a6a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 6 Aug 2025 10:27:13 +0200 Subject: [PATCH 2/3] [enum] re-use existing const --- .../array_filter_with_docblock_type.php.inc | 33 +++++++ .../Fixture/include_array_map.php.inc | 33 +++++++ .../Fixture/skip_unclear_param.php.inc | 14 +++ .../Enum/NativeFuncCallPositions.php | 30 +++++++ ...ockBasedOnCallableNativeFuncCallRector.php | 37 ++------ ...AddArrayFunctionClosureParamTypeRector.php | 89 ++++++++++++------- .../Output/GitHubOutputFormatter.php | 5 +- 7 files changed, 177 insertions(+), 64 deletions(-) create mode 100644 rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/array_filter_with_docblock_type.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/include_array_map.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector/Fixture/skip_unclear_param.php.inc create mode 100644 rules/TypeDeclaration/Enum/NativeFuncCallPositions.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/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 index a1fa9502c5f..8c28029fccb 100644 --- a/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php +++ b/rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php @@ -8,10 +8,13 @@ use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; +use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\MixedType; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; use Rector\Rector\AbstractRector; use Rector\StaticTypeMapper\StaticTypeMapper; +use Rector\TypeDeclaration\Enum\NativeFuncCallPositions; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -29,20 +32,24 @@ public function __construct( public function getRuleDefinition(): RuleDefinition { - return new RuleDefinition('Add array_filter() function closure param type, based on passed iterable', [ - new CodeSample( - <<<'CODE_SAMPLE' + return new RuleDefinition( + 'Add array_filter()/array_map() function closure param type, based on passed iterable', + [ + new CodeSample( + <<<'CODE_SAMPLE' $items = [1, 2, 3]; $result = array_filter($items, fn ($item) => $item > 1); CODE_SAMPLE - , - <<<'CODE_SAMPLE' + , + <<<'CODE_SAMPLE' $items = [1, 2, 3]; $result = array_filter($items, fn (int $item) => $item > 1 CODE_SAMPLE - ), - ]); + ), + + ] + ); } /** @@ -58,44 +65,58 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if (! $this->isName($node, 'array_filter')) { - return null; - } + foreach (NativeFuncCallPositions::ARRAY_AND_CALLBACK_POSITIONS as $functionName => $positions) { + if (! $this->isName($node, $functionName)) { + continue; + } - if ($node->isFirstClassCallable()) { - return null; - } + if ($node->isFirstClassCallable()) { + continue; + } - $firstArgExpr = $node->getArgs()[1] - ->value; - if (! $firstArgExpr instanceof ArrowFunction && ! $firstArgExpr instanceof Closure) { - return null; - } + $arrayPosition = $positions['array']; + $callbackPosition = $positions['callback']; - $arrowFunction = $firstArgExpr; - $arrowFunctionParam = $arrowFunction->getParams()[0]; + $firstArgExpr = $node->getArgs()[$callbackPosition] + ->value; + if (! $firstArgExpr instanceof ArrowFunction && ! $firstArgExpr instanceof Closure) { + continue; + } - // param is known already - if ($arrowFunctionParam->type instanceof Node) { - return null; - } + $arrowFunction = $firstArgExpr; + $arrowFunctionParam = $arrowFunction->getParams()[0]; - $passedExprType = $this->getType($node->getArgs()[0]->value); - if ($passedExprType instanceof ConstantArrayType) { - $singlePassedExprType = $passedExprType->getItemType(); + // param is known already + if ($arrowFunctionParam->type instanceof Node) { + continue; + } - $paramType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($singlePassedExprType, TypeKind::PARAM); + $passedExprType = $this->getType($node->getArgs()[$arrayPosition]->value); + if ($passedExprType instanceof ConstantArrayType || $passedExprType instanceof ArrayType) { + $singlePassedExprType = $passedExprType->getItemType(); - if (! $paramType instanceof Node) { - return null; - } + if ($singlePassedExprType instanceof MixedType) { + continue; + } + + $paramType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $singlePassedExprType, + TypeKind::PARAM + ); - $arrowFunctionParam->type = $paramType; + if (! $paramType instanceof Node) { + continue; + } - return $node; + $arrowFunctionParam->type = $paramType; + + return $node; + } + + return null; } - return null; + return $node; } public function provideMinPhpVersion(): int diff --git a/src/ChangesReporting/Output/GitHubOutputFormatter.php b/src/ChangesReporting/Output/GitHubOutputFormatter.php index 27ea6b3bf45..025a302072a 100644 --- a/src/ChangesReporting/Output/GitHubOutputFormatter.php +++ b/src/ChangesReporting/Output/GitHubOutputFormatter.php @@ -125,7 +125,10 @@ 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 (int|string|null $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)), From a9ae71f78b696e34e75e6b43e61c52a56af99d26 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 6 Aug 2025 08:40:43 +0000 Subject: [PATCH 3/3] [ci-review] Rector Rectify --- .../FunctionLike/AddClosureParamTypeForArrayMapRector.php | 3 ++- src/ChangesReporting/Output/GitHubOutputFormatter.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 025a302072a..8516157d619 100644 --- a/src/ChangesReporting/Output/GitHubOutputFormatter.php +++ b/src/ChangesReporting/Output/GitHubOutputFormatter.php @@ -131,7 +131,7 @@ private function sanitizeAnnotationProperties(array $annotationProperties): stri ); $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 );