diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index ddf1bf381e2..c9877225baf 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -50,7 +50,7 @@ jobs: - name: 'Finalize classes' - run: vendor/bin/swiss-knife finalize-classes src tests + run: vendor/bin/swiss-knife finalize-classes src tests --dry-run - name: 'Detect composer dependency issues' diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/AddReturnTypeFromTryCatchTypeRectorTest.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/AddReturnTypeFromTryCatchTypeRectorTest.php new file mode 100644 index 00000000000..ef0766b30d1 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/AddReturnTypeFromTryCatchTypeRectorTest.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/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/simple_try_catch.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/simple_try_catch.php.inc new file mode 100644 index 00000000000..e146312ebe4 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/simple_try_catch.php.inc @@ -0,0 +1,35 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/skip_doc_type.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/skip_doc_type.php.inc new file mode 100644 index 00000000000..29f9b436f08 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/skip_doc_type.php.inc @@ -0,0 +1,23 @@ +getIntDoc(); + } + } + + /** + * @return int + */ + private function getIntDoc() + { + return mt_rand(0, 1) ? 'string' : 1; + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/skip_if_no_catch.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/skip_if_no_catch.php.inc new file mode 100644 index 00000000000..ae456d0762d --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector/Fixture/skip_if_no_catch.php.inc @@ -0,0 +1,15 @@ +rule(AddReturnTypeFromTryCatchTypeRector::class); +}; diff --git a/rules/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector.php b/rules/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector.php index 7c27c13c837..5a949d5f9da 100644 --- a/rules/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector.php +++ b/rules/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector.php @@ -28,7 +28,7 @@ /** * @see \Rector\Tests\Transform\Rector\ArrayDimFetch\ArrayDimFetchToMethodCallRector\ArrayDimFetchToMethodCallRectorTest */ -class ArrayDimFetchToMethodCallRector extends AbstractRector implements ConfigurableRectorInterface +final class ArrayDimFetchToMethodCallRector extends AbstractRector implements ConfigurableRectorInterface { /** * @var ArrayDimFetchToMethodCall[] @@ -101,16 +101,16 @@ public function configure(array $configuration): void $this->arrayDimFetchToMethodCalls = $configuration; } - private function handleIsset(Isset_ $node): Expr|int|null + private function handleIsset(Isset_ $isset): Expr|int|null { $issets = []; $exprs = []; - foreach ($node->vars as $var) { + foreach ($isset->vars as $var) { if ($var instanceof ArrayDimFetch) { $methodCall = $this->getMethodCall($var, 'exists'); - if ($methodCall !== null) { + if ($methodCall instanceof MethodCall) { $exprs[] = $methodCall; continue; } @@ -124,13 +124,13 @@ private function handleIsset(Isset_ $node): Expr|int|null } if ($issets !== []) { - $node->vars = $issets; - array_unshift($exprs, $node); + $isset->vars = $issets; + array_unshift($exprs, $isset); } return array_reduce( $exprs, - fn (?Expr $carry, Expr $expr) => $carry === null ? $expr : new BooleanAnd($carry, $expr), + fn (?Expr $carry, Expr $expr): Isset_|MethodCall|BooleanAnd => $carry instanceof Expr ? new BooleanAnd($carry, $expr) : $expr, null, ); } @@ -138,16 +138,16 @@ private function handleIsset(Isset_ $node): Expr|int|null /** * @return Stmt[]|int */ - private function handleUnset(Unset_ $node): array|int + private function handleUnset(Unset_ $unset): array|int { $unsets = []; $stmts = []; - foreach ($node->vars as $var) { + foreach ($unset->vars as $var) { if ($var instanceof ArrayDimFetch) { $methodCall = $this->getMethodCall($var, 'unset'); - if ($methodCall !== null) { + if ($methodCall instanceof MethodCall) { $stmts[] = new Expression($methodCall); continue; } @@ -161,8 +161,8 @@ private function handleUnset(Unset_ $node): array|int } if ($unsets !== []) { - $node->vars = $unsets; - array_unshift($stmts, $node); + $unset->vars = $unsets; + array_unshift($stmts, $unset); } return $stmts; @@ -171,14 +171,14 @@ private function handleUnset(Unset_ $node): array|int /** * @param 'get'|'set'|'exists'|'unset' $action */ - private function getMethodCall(ArrayDimFetch $fetch, string $action, ?Expr $value = null): ?MethodCall + private function getMethodCall(ArrayDimFetch $arrayDimFetch, string $action, ?Expr $expr = null): ?MethodCall { - if (!$fetch->dim instanceof Node) { + if (!$arrayDimFetch->dim instanceof Node) { return null; } foreach ($this->arrayDimFetchToMethodCalls as $arrayDimFetchToMethodCall) { - if (!$this->isObjectType($fetch->var, $arrayDimFetchToMethodCall->getObjectType())) { + if (!$this->isObjectType($arrayDimFetch->var, $arrayDimFetchToMethodCall->getObjectType())) { continue; } @@ -193,13 +193,13 @@ private function getMethodCall(ArrayDimFetch $fetch, string $action, ?Expr $valu continue; } - $args = [new Arg($fetch->dim)]; + $args = [new Arg($arrayDimFetch->dim)]; - if ($value instanceof Expr) { - $args[] = new Arg($value); + if ($expr instanceof Expr) { + $args[] = new Arg($expr); } - return new MethodCall($fetch->var, $method, $args); + return new MethodCall($arrayDimFetch->var, $method, $args); } return null; diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector.php new file mode 100644 index 00000000000..6f1eeacf62b --- /dev/null +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnTypeFromTryCatchTypeRector.php @@ -0,0 +1,168 @@ +> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + $scope = ScopeFetcher::fetch($node); + if ($this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { + return null; + } + + // already known type + if ($node->returnType instanceof Node) { + return null; + } + + $tryReturnType = null; + $catchReturnTypes = []; + + $returns = $this->betterNodeFinder->findReturnsScoped($node); + if (! $this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returns)) { + return null; + } + + foreach ((array) $node->stmts as $classMethodStmt) { + if (! $classMethodStmt instanceof TryCatch) { + continue; + } + + // skip if there is no catch + if ($classMethodStmt->catches === []) { + continue; + } + + $tryCatch = $classMethodStmt; + $tryReturnType = $this->matchReturnType($tryCatch); + + foreach ($tryCatch->catches as $catch) { + $currentCatchType = $this->matchReturnType($catch); + + // each catch must have type + if (! $currentCatchType instanceof Type) { + return null; + } + + $catchReturnTypes[] = $currentCatchType; + } + } + + if (! $tryReturnType instanceof Type) { + return null; + } + + foreach ($catchReturnTypes as $catchReturnType) { + if (! $this->typeComparator->areTypesEqual($catchReturnType, $tryReturnType)) { + return null; + } + } + + $returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($tryReturnType, TypeKind::RETURN); + if (! $returnType instanceof Node) { + return null; + } + + $node->returnType = $returnType; + return $node; + } + + private function matchReturnType(TryCatch|Catch_ $tryOrCatch): ?Type + { + foreach ($tryOrCatch->stmts as $stmt) { + if (! $stmt instanceof Return_) { + continue; + } + + if (! $stmt->expr instanceof Expr) { + continue; + } + + return $this->nodeTypeResolver->getNativeType($stmt->expr); + } + + return null; + } +} diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index 0728c1c7ca5..a9d34e64c88 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -18,6 +18,7 @@ use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeBasedOnPHPUnitDataProviderRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromPropertyTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeDeclarationBasedOnParentClassMethodRector; +use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnTypeFromTryCatchTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\ClassMethod\BoolReturnTypeFromBooleanConstReturnsRector; use Rector\TypeDeclaration\Rector\ClassMethod\BoolReturnTypeFromBooleanStrictReturnsRector; @@ -108,6 +109,7 @@ final class TypeDeclarationLevel AddParamTypeBasedOnPHPUnitDataProviderRector::class, TypedPropertyFromStrictSetUpRector::class, ReturnTypeFromStrictNativeCallRector::class, + AddReturnTypeFromTryCatchTypeRector::class, ReturnTypeFromStrictTypedCallRector::class, ChildDoctrineRepositoryClassTypeRector::class, diff --git a/tests/Configuration/Source/RemoveDoubleAssignRector.php b/tests/Configuration/Source/RemoveDoubleAssignRector.php index 90451779eb2..4d6c05bdc49 100644 --- a/tests/Configuration/Source/RemoveDoubleAssignRector.php +++ b/tests/Configuration/Source/RemoveDoubleAssignRector.php @@ -9,7 +9,7 @@ /** * Dummy rector with same class name as an official one */ -class RemoveDoubleAssignRector extends AbstractRector +final class RemoveDoubleAssignRector extends AbstractRector { public function getRuleDefinition(): RuleDefinition { diff --git a/tests/FileSystem/FilesFinder/SourceWithSuffix/SomeController.php b/tests/FileSystem/FilesFinder/SourceWithSuffix/SomeController.php index 818c388f626..94bc266f596 100644 --- a/tests/FileSystem/FilesFinder/SourceWithSuffix/SomeController.php +++ b/tests/FileSystem/FilesFinder/SourceWithSuffix/SomeController.php @@ -2,6 +2,6 @@ namespace FileSystem\FilesFinder\SourceWithSuffix; -class SomeController +final class SomeController { } diff --git a/tests/FileSystem/FilesFinder/SourceWithSuffix/SomeRepository.php b/tests/FileSystem/FilesFinder/SourceWithSuffix/SomeRepository.php index 9103bc6e35a..7e81cc91731 100644 --- a/tests/FileSystem/FilesFinder/SourceWithSuffix/SomeRepository.php +++ b/tests/FileSystem/FilesFinder/SourceWithSuffix/SomeRepository.php @@ -2,6 +2,6 @@ namespace FileSystem\FilesFinder\SourceWithSuffix; -class SomeRepository +final class SomeRepository { } diff --git a/tests/FileSystem/FilesFinder/SourceWithSuffix/other_unrelated_file.php b/tests/FileSystem/FilesFinder/SourceWithSuffix/other_unrelated_file.php index 726c3e00df4..577e97b30a0 100644 --- a/tests/FileSystem/FilesFinder/SourceWithSuffix/other_unrelated_file.php +++ b/tests/FileSystem/FilesFinder/SourceWithSuffix/other_unrelated_file.php @@ -2,6 +2,6 @@ namespace FileSystem\FilesFinder\SourceWithSuffix; -class OtherUnrelatedFile +final class OtherUnrelatedFile { } diff --git a/tests/Issues/AutoImport/Source/Docblock/DateTime.php b/tests/Issues/AutoImport/Source/Docblock/DateTime.php index 761156d7529..1e780410835 100644 --- a/tests/Issues/AutoImport/Source/Docblock/DateTime.php +++ b/tests/Issues/AutoImport/Source/Docblock/DateTime.php @@ -4,6 +4,6 @@ namespace Rector\Tests\Issues\AutoImport\Source\Docblock; -class DateTime +final class DateTime { }