diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/AddReturnDocblockDataProviderRectorTest.php b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/AddReturnDocblockDataProviderRectorTest.php new file mode 100644 index 00000000000..51ee9981864 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/AddReturnDocblockDataProviderRectorTest.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/Class_/AddReturnDocblockDataProviderRector/Fixture/data_provider_method.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/data_provider_method.php.inc new file mode 100644 index 00000000000..a30db6c0761 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/data_provider_method.php.inc @@ -0,0 +1,52 @@ + +----- + diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/with_integers.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/with_integers.php.inc new file mode 100644 index 00000000000..59e2e5f1553 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/Fixture/with_integers.php.inc @@ -0,0 +1,52 @@ + +----- + diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/config/configured_rule.php b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/config/configured_rule.php new file mode 100644 index 00000000000..60e43dbe171 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(AddReturnDocblockDataProviderRector::class); +}; diff --git a/rules/CodingStyle/Rector/Enum_/EnumCaseToPascalCaseRector.php b/rules/CodingStyle/Rector/Enum_/EnumCaseToPascalCaseRector.php index 7d64f768521..130166d9d55 100644 --- a/rules/CodingStyle/Rector/Enum_/EnumCaseToPascalCaseRector.php +++ b/rules/CodingStyle/Rector/Enum_/EnumCaseToPascalCaseRector.php @@ -194,9 +194,9 @@ private function convertToPascalCase(string $name): string fn ($part): string => // If part is all uppercase, convert to ucfirst(strtolower()) // If part is already mixed or PascalCase, keep as is except ucfirst - ctype_upper((string) $part) - ? ucfirst(strtolower((string) $part)) - : ucfirst((string) $part), + ctype_upper($part) + ? ucfirst(strtolower($part)) + : ucfirst($part), $parts ) ); diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index d5579044d65..1ae74ca35e5 100644 --- a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php +++ b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php @@ -77,20 +77,31 @@ private function shouldSkip( ): bool { $params = $node->getParams(); $args = $callLike->getArgs(); + if ($callLike->isFirstClassCallable()) { + return true; + } - if ( - $callLike->isFirstClassCallable() - || $this->isChainedCall($callLike) - || $this->isUsingNamedArgs($args) - || $this->isUsingByRef($params) - || $this->isNotUsingSameParamsForArgs($params, $args) - || $this->isDependantMethod($callLike, $params) - || $this->isUsingThisInNonObjectContext($callLike, $scope) - ) { + if ($this->isChainedCall($callLike)) { return true; } - return false; + if ($this->isUsingNamedArgs($args)) { + return true; + } + + if ($this->isUsingByRef($params)) { + return true; + } + + if ($this->isNotUsingSameParamsForArgs($params, $args)) { + return true; + } + + if ($this->isDependantMethod($callLike, $params)) { + return true; + } + + return $this->isUsingThisInNonObjectContext($callLike, $scope); } private function extractCallLike(Closure|ArrowFunction $node): FuncCall|MethodCall|StaticCall|null @@ -99,6 +110,7 @@ private function extractCallLike(Closure|ArrowFunction $node): FuncCall|MethodCa if (count($node->stmts) !== 1 || ! $node->stmts[0] instanceof Return_) { return null; } + $callLike = $node->stmts[0]->expr; } else { $callLike = $node->expr; @@ -198,6 +210,7 @@ private function isUsingByRef(array $params): bool return true; } } + return false; } @@ -211,6 +224,7 @@ private function isUsingNamedArgs(array $args): bool return true; } } + return false; } @@ -220,10 +234,6 @@ private function isChainedCall(FuncCall|MethodCall|StaticCall $callLike): bool return false; } - if (! $callLike->var instanceof CallLike) { - return false; - } - - return true; + return $callLike->var instanceof CallLike; } } diff --git a/rules/Privatization/TypeManipulator/TypeNormalizer.php b/rules/Privatization/TypeManipulator/TypeNormalizer.php index bffaa4fd540..b00720ed0bb 100644 --- a/rules/Privatization/TypeManipulator/TypeNormalizer.php +++ b/rules/Privatization/TypeManipulator/TypeNormalizer.php @@ -4,26 +4,30 @@ namespace Rector\Privatization\TypeManipulator; +use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\UnionType; final class TypeNormalizer { /** - * Generalize false/true type to bool, + * Generalize false/true constantArrayType to bool, * as mostly default value but accepts both */ public function generalizeConstantBoolTypes(Type $type): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverseCallback): BooleanType|Type { + return TypeTraverser::map($type, function (Type $type, callable $traverseCallback): BooleanType|Type { if ($type instanceof ConstantBooleanType) { return new BooleanType(); } @@ -40,7 +44,45 @@ public function generalizeConstantBoolTypes(Type $type): Type return new IntegerType(); } + if ($type instanceof ConstantArrayType) { + // is relevant int constantArrayType? + if ($this->isImplicitNumberedListKeyType($type)) { + $keyType = new MixedType(); + } else { + $keyType = $traverseCallback($type->getKeyType(), $traverseCallback); + } + + // should be string[] + $itemType = $traverseCallback($type->getItemType(), $traverseCallback); + if ($itemType instanceof ConstantStringType) { + $itemType = new StringType(); + } + + return new ArrayType($keyType, $itemType); + } + return $traverseCallback($type, $traverseCallback); }); } + + private function isImplicitNumberedListKeyType(ConstantArrayType $constantArrayType): bool + { + if (! $constantArrayType->getKeyType() instanceof UnionType) { + return false; + } + + foreach ($constantArrayType->getKeyType()->getTypes() as $key => $keyType) { + if ($keyType instanceof ConstantIntegerType) { + if ($keyType->getValue() === $key) { + continue; + } + + return false; + } + + return false; + } + + return true; + } } diff --git a/rules/TypeDeclarationDocblocks/NodeFinder/DataProviderMethodsFinder.php b/rules/TypeDeclarationDocblocks/NodeFinder/DataProviderMethodsFinder.php index 3670320368c..bad7624d35e 100644 --- a/rules/TypeDeclarationDocblocks/NodeFinder/DataProviderMethodsFinder.php +++ b/rules/TypeDeclarationDocblocks/NodeFinder/DataProviderMethodsFinder.php @@ -21,6 +21,24 @@ public function __construct( ) { } + /** + * @return ClassMethod[] + */ + public function findDataProviderNodesInClass(Class_ $class): array + { + $dataProviderClassMethods = []; + + foreach ($class->getMethods() as $classMethod) { + $currentDataProviderNodes = $this->findDataProviderNodes($class, $classMethod); + $dataProviderClassMethods = array_merge( + $dataProviderClassMethods, + $currentDataProviderNodes->getClassMethods() + ); + } + + return $dataProviderClassMethods; + } + public function findDataProviderNodes(Class_ $class, ClassMethod $classMethod): DataProviderNodes { $phpDocInfo = $this->phpDocInfoFactory->createFromNode($classMethod); diff --git a/rules/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector.php b/rules/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector.php new file mode 100644 index 00000000000..79b9d2f1b9b --- /dev/null +++ b/rules/TypeDeclarationDocblocks/Rector/Class_/AddReturnDocblockDataProviderRector.php @@ -0,0 +1,156 @@ +> + */ + public function provideItems() + { + return [ + [['item1', 'item2']], + [['item3', 'item4']], + ]; + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + $hasChanged = false; + + $dataProviderClassMethods = $this->dataProviderMethodsFinder->findDataProviderNodesInClass($node); + + if ($dataProviderClassMethods === []) { + return null; + } + + foreach ($dataProviderClassMethods as $dataProviderClassMethod) { + $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($dataProviderClassMethod); + + $returnTagValueNode = $classMethodPhpDocInfo->getReturnTagValue(); + + // already set + if ($returnTagValueNode instanceof ReturnTagValueNode) { + continue; + } + + $soleReturn = $this->returnNodeFinder->findOnlyReturnWithExpr($dataProviderClassMethod); + + // unable to resolve type + if (! $soleReturn instanceof Return_) { + continue; + } + + if (! $soleReturn->expr instanceof Expr) { + continue; + } + + $soleReturnType = $this->getType($soleReturn->expr); + + $generalizedReturnType = $this->typeNormalizer->generalizeConstantBoolTypes($soleReturnType); + + // turn into rather generic short return type, to keep it open to extension later and readable to human + $docType = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($generalizedReturnType); + + $returnTagValueNode = new ReturnTagValueNode($docType, ''); + $classMethodPhpDocInfo->addTagValueNode($returnTagValueNode); + + $hasChanged = true; + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($dataProviderClassMethod); + } + + if (! $hasChanged) { + return null; + } + + return $node; + } +}