diff --git a/rules-tests/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector/ClosureReturnTypeFromAssertInstanceOfRectorTest.php b/rules-tests/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector/ClosureReturnTypeFromAssertInstanceOfRectorTest.php new file mode 100644 index 00000000000..d4c301d5713 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector/ClosureReturnTypeFromAssertInstanceOfRectorTest.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/Closure/ClosureReturnTypeFromAssertInstanceOfRector/Fixture/assert_instance_of_narrowed_return.php.inc b/rules-tests/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector/Fixture/assert_instance_of_narrowed_return.php.inc new file mode 100644 index 00000000000..ec6922f0f4c --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector/Fixture/assert_instance_of_narrowed_return.php.inc @@ -0,0 +1,39 @@ +assertInstanceOf(\DateTime::class, $object); + + return $object; + }; + } +} + +?> +----- +assertInstanceOf(\DateTime::class, $object); + + return $object; + }; + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector/Fixture/skip_no_assert_instance_of.php.inc b/rules-tests/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector/Fixture/skip_no_assert_instance_of.php.inc new file mode 100644 index 00000000000..2a3f4c5dc28 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector/Fixture/skip_no_assert_instance_of.php.inc @@ -0,0 +1,15 @@ +rule(ClosureReturnTypeFromAssertInstanceOfRector::class); + $rectorConfig->phpVersion(PhpVersion::PHP_82); +}; diff --git a/rules/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector.php b/rules/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector.php new file mode 100644 index 00000000000..db28fe6a4c0 --- /dev/null +++ b/rules/TypeDeclaration/Rector/Closure/ClosureReturnTypeFromAssertInstanceOfRector.php @@ -0,0 +1,149 @@ +assertInstanceOf(SomeType::class, $object); + + return $object; + }; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +final class SomeTest extends TestCase +{ + public function test() + { + $callback = function (object $object): SomeType { + $this->assertInstanceOf(SomeType::class, $object); + + return $object; + }; + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Closure::class]; + } + + /** + * @param Closure $node + */ + public function refactor(Node $node): ?Node + { + // type is already set + if ($node->returnType instanceof Node) { + return null; + } + + $scope = ScopeFetcher::fetch($node); + if (! $this->isInsideTestCaseClass($scope)) { + return null; + } + + // only handle closures narrowed by assertInstanceOf(); plain returns are handled by ClosureReturnTypeRector + if (! $this->hasAssertInstanceOf($node)) { + return null; + } + + $closureReturnType = $this->returnTypeInferer->inferFunctionLike($node); + + // handled by other rules + if ($closureReturnType instanceof NeverType) { + return null; + } + + $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($closureReturnType, TypeKind::RETURN); + if (! $returnTypeNode instanceof Node) { + return null; + } + + $node->returnType = $returnTypeNode; + + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::SCALAR_TYPES; + } + + private function hasAssertInstanceOf(Closure $closure): bool + { + return (bool) $this->betterNodeFinder->findFirst( + $closure->stmts, + fn (Node $node): bool => ($node instanceof MethodCall || $node instanceof StaticCall) + && $this->isName($node->name, 'assertInstanceOf') + ); + } + + private function isInsideTestCaseClass(Scope $scope): bool + { + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return false; + } + + // is phpunit test case? + return $classReflection->is(ClassName::TEST_CASE_CLASS); + } +} diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index 5d22033731f..bbb63907a4d 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -54,6 +54,7 @@ use Rector\TypeDeclaration\Rector\ClassMethod\StringReturnTypeFromStrictStringReturnsRector; use Rector\TypeDeclaration\Rector\Closure\AddClosureNeverReturnTypeRector; use Rector\TypeDeclaration\Rector\Closure\AddClosureVoidReturnTypeWhereNoReturnRector; +use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeFromAssertInstanceOfRector; use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeRector; use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector; use Rector\TypeDeclaration\Rector\FuncCall\AddArrayAnyAllClosureParamTypeRector; @@ -174,5 +175,6 @@ final class TypeDeclarationLevel // PHP 8.4 NarrowArrayAnyAllNullableParamTypeRector::class, AddArrayAnyAllClosureParamTypeRector::class, + ClosureReturnTypeFromAssertInstanceOfRector::class, ]; }