diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/AddParamArrayDocblockBasedOnArrayMapRectorTest.php b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/AddParamArrayDocblockBasedOnArrayMapRectorTest.php new file mode 100644 index 00000000000..14244050c09 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/AddParamArrayDocblockBasedOnArrayMapRectorTest.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/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/override_mixed_type.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/override_mixed_type.php.inc new file mode 100644 index 00000000000..461858d30de --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/override_mixed_type.php.inc @@ -0,0 +1,33 @@ + trim($item), $items); + } +} + +?> +----- + trim($item), $items); + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/skip_better_existing_type.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/skip_better_existing_type.php.inc new file mode 100644 index 00000000000..765fd64628f --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/skip_better_existing_type.php.inc @@ -0,0 +1,14 @@ + $items + */ + public function run(array $items): void + { + array_map(fn (string $item) => trim($item), $items); + } +} diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/some_class.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/some_class.php.inc new file mode 100644 index 00000000000..14627faf497 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/Fixture/some_class.php.inc @@ -0,0 +1,30 @@ + trim($item), $items); + } +} + +?> +----- + trim($item), $items); + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/config/configured_rule.php b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/config/configured_rule.php new file mode 100644 index 00000000000..8be29c33185 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([AddParamArrayDocblockBasedOnArrayMapRector::class]); diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php index 3cd5e4dec0d..29737b3ed6f 100644 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php +++ b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php @@ -50,7 +50,11 @@ public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedO * on php 7.x, substr() result can return false, so force (string) is needed * @see https://github.com/rectorphp/rector-src/pull/7436 */ - $subClassName = (string) substr($fullyQualifiedObjectType->getClassName(), 0, -strlen($fullyQualifiedObjectType->getShortName()) - 1); + $subClassName = (string) substr( + $fullyQualifiedObjectType->getClassName(), + 0, + -strlen($fullyQualifiedObjectType->getShortName()) - 1 + ); $fullyQualifiedObjectTypeNamespace = strtolower($subClassName); foreach ($classLikeNames as $classLikeName) { diff --git a/rules/TypeDeclarationDocblocks/NodeFinder/ArrayMapClosureExprFinder.php b/rules/TypeDeclarationDocblocks/NodeFinder/ArrayMapClosureExprFinder.php new file mode 100644 index 00000000000..031f2a137f2 --- /dev/null +++ b/rules/TypeDeclarationDocblocks/NodeFinder/ArrayMapClosureExprFinder.php @@ -0,0 +1,66 @@ + + */ + public function findByVariableName(ClassMethod|Function_ $functionLike, string $variableName): array + { + if ($functionLike->stmts === null) { + return []; + } + + /** @var FuncCall[] $funcCalls */ + $funcCalls = $this->betterNodeFinder->findInstancesOfScoped($functionLike->stmts, FuncCall::class); + + $arrayMapClosures = []; + + foreach ($funcCalls as $funcCall) { + if ($funcCall->isFirstClassCallable()) { + continue; + } + + if (! $this->nodeNameResolver->isName($funcCall, 'array_map')) { + continue; + } + + $secondArg = $funcCall->getArgs()[1]; + if (! $secondArg->value instanceof Variable) { + continue; + } + + if (! $this->nodeNameResolver->isName($secondArg->value, $variableName)) { + continue; + } + + $firstArg = $funcCall->getArgs()[0]; + if (! $firstArg->value instanceof Closure && ! $firstArg->value instanceof ArrowFunction) { + continue; + } + + $arrayMapClosures[] = $firstArg->value; + } + + return $arrayMapClosures; + } +} diff --git a/rules/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector.php b/rules/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector.php new file mode 100644 index 00000000000..b8f2ae415bd --- /dev/null +++ b/rules/TypeDeclarationDocblocks/Rector/ClassMethod/AddParamArrayDocblockBasedOnArrayMapRector.php @@ -0,0 +1,177 @@ + trim($name), $names); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class SomeClass +{ + /** + * @param string[] $names + */ + public function run(array $names): void + { + $names = array_map(fn(string $name) => trim($name), $names); + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class, Function_::class]; + } + + /** + * @param ClassMethod|Function_ $node + */ + public function refactor(Node $node): ?Node + { + if ($node->getParams() === []) { + return null; + } + + $hasChanged = false; + $functionPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); + + foreach ($node->params as $param) { + // handle only arrays + if (! $this->isArrayParam($param)) { + continue; + } + + $paramName = $this->getName($param); + + $arrayMapClosures = $this->arrayMapClosureExprFinder->findByVariableName($node, $paramName); + if ($arrayMapClosures === []) { + continue; + } + + foreach ($arrayMapClosures as $arrayMapClosure) { + $params = $arrayMapClosure->getParams(); + if ($params === []) { + continue; + } + + $firstParam = $params[0]; + $paramTypeNode = $firstParam->type; + if ($paramTypeNode === null) { + continue; + } + + $paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($paramTypeNode); + $arrayParamType = new ArrayType(new MixedType(), $paramType); + + if ($this->isAlreadyNonMixedParamType($functionPhpDocInfo, $paramName)) { + continue; + } + + $this->phpDocTypeChanger->changeParamType( + $node, + $functionPhpDocInfo, + $arrayParamType, + $param, + $paramName + ); + $hasChanged = true; + } + + } + + if (! $hasChanged) { + return null; + } + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + private function isArrayParam(Param $param): bool + { + if (! $param->type instanceof Identifier) { + return false; + } + + return $this->isName($param->type, 'array'); + } + + private function isMixedArrayType(Type $type): bool + { + if (! $type instanceof ArrayType) { + return false; + } + + if (! $type->getItemType() instanceof MixedType) { + return false; + } + + return $type->getKeyType() instanceof MixedType; + } + + private function isAlreadyNonMixedParamType( + PhpDocInfo $functionPhpDocInfo, + string $paramName + ): bool { + $currentParamType = $functionPhpDocInfo->getParamType($paramName); + if ($currentParamType instanceof MixedType) { + return false; + } + + // has useful param type already? + return ! $this->isMixedArrayType($currentParamType); + } +} diff --git a/src/Config/Level/DeadCodeLevel.php b/src/Config/Level/DeadCodeLevel.php index 19256a91236..e86bccb412f 100644 --- a/src/Config/Level/DeadCodeLevel.php +++ b/src/Config/Level/DeadCodeLevel.php @@ -26,8 +26,8 @@ use Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnExprInConstructRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector; -use Rector\DeadCode\Rector\Concat\RemoveConcatAutocastRector; use Rector\DeadCode\Rector\Closure\RemoveUnusedClosureVariableUseRector; +use Rector\DeadCode\Rector\Concat\RemoveConcatAutocastRector; use Rector\DeadCode\Rector\ConstFetch\RemovePhpVersionIdCheckRector; use Rector\DeadCode\Rector\Expression\RemoveDeadStmtRector; use Rector\DeadCode\Rector\Expression\SimplifyMirrorAssignRector; diff --git a/src/Config/Level/TypeDeclarationDocblocksLevel.php b/src/Config/Level/TypeDeclarationDocblocksLevel.php index f9cb4f550f1..2bb606c2eeb 100644 --- a/src/Config/Level/TypeDeclarationDocblocksLevel.php +++ b/src/Config/Level/TypeDeclarationDocblocksLevel.php @@ -12,6 +12,7 @@ use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarArrayFromGetterReturnRector; use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarArrayFromPropertyDefaultsRector; use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarFromParamDocblockInConstructorRector; +use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockBasedOnArrayMapRector; use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockFromDataProviderRector; use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockFromDimFetchAccessRector; use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForArrayDimAssignedObjectRector; @@ -37,6 +38,7 @@ final class TypeDeclarationDocblocksLevel // param AddParamArrayDocblockFromDimFetchAccessRector::class, ClassMethodArrayDocblockParamFromLocalCallsRector::class, + AddParamArrayDocblockBasedOnArrayMapRector::class, // return AddReturnDocblockForCommonObjectDenominatorRector::class,