diff --git a/src/Application/VersionResolver.php b/src/Application/VersionResolver.php index 4083241da2d..fbe8ecdd0c3 100644 --- a/src/Application/VersionResolver.php +++ b/src/Application/VersionResolver.php @@ -19,12 +19,12 @@ final class VersionResolver * @api * @var string */ - public const PACKAGE_VERSION = '8281a06bc8432d9d7f8e20842e3961df99a277bc'; + public const PACKAGE_VERSION = '9bcded03ea2db7b01183aad2407e3f7490be9531'; /** * @api * @var string */ - public const RELEASE_DATE = '2024-04-24 08:54:03'; + public const RELEASE_DATE = '2024-04-25 10:20:45'; /** * @var int */ diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index 532d1a6d439..3bd5c88eddc 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -289,6 +289,9 @@ final class LazyContainerFactory $phpStanServicesFactory = $container->make(PHPStanServicesFactory::class); return $phpStanServicesFactory->createDynamicSourceLocatorProvider(); }); + $rectorConfig->afterResolving(DynamicSourceLocatorProvider::class, static function (DynamicSourceLocatorProvider $dynamicSourceLocatorProvider, Container $container) : void { + $dynamicSourceLocatorProvider->autowire($container->make(ReflectionProvider::class)); + }); // resetables $rectorConfig->tag(DynamicSourceLocatorProvider::class, ResetableInterface::class); $rectorConfig->tag(RenamedClassesDataCollector::class, ResetableInterface::class); diff --git a/src/FamilyTree/Reflection/FamilyRelationsAnalyzer.php b/src/FamilyTree/Reflection/FamilyRelationsAnalyzer.php index 9915012f6ac..ca433d087c8 100644 --- a/src/FamilyTree/Reflection/FamilyRelationsAnalyzer.php +++ b/src/FamilyTree/Reflection/FamilyRelationsAnalyzer.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt\Interface_; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; use Rector\NodeNameResolver\NodeNameResolver; +use Rector\Util\Reflection\PrivatesAccessor; final class FamilyRelationsAnalyzer { /** @@ -21,10 +22,35 @@ final class FamilyRelationsAnalyzer * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; - public function __construct(ReflectionProvider $reflectionProvider, NodeNameResolver $nodeNameResolver) + /** + * @readonly + * @var \Rector\Util\Reflection\PrivatesAccessor + */ + private $privatesAccessor; + public function __construct(ReflectionProvider $reflectionProvider, NodeNameResolver $nodeNameResolver, PrivatesAccessor $privatesAccessor) { $this->reflectionProvider = $reflectionProvider; $this->nodeNameResolver = $nodeNameResolver; + $this->privatesAccessor = $privatesAccessor; + } + /** + * @return ClassReflection[] + */ + public function getChildrenOfClassReflection(ClassReflection $desiredClassReflection) : array + { + if ($desiredClassReflection->isFinalByKeyword()) { + return []; + } + /** @var ClassReflection[] $classReflections */ + $classReflections = $this->privatesAccessor->getPrivateProperty($this->reflectionProvider, 'classes'); + $childrenClassReflections = []; + foreach ($classReflections as $classReflection) { + if (!$classReflection->isSubclassOf($desiredClassReflection->getName())) { + continue; + } + $childrenClassReflections[] = $classReflection; + } + return $childrenClassReflections; } /** * @api diff --git a/src/NodeTypeResolver/Reflection/BetterReflection/SourceLocatorProvider/DynamicSourceLocatorProvider.php b/src/NodeTypeResolver/Reflection/BetterReflection/SourceLocatorProvider/DynamicSourceLocatorProvider.php index 69e29e97771..69f7a294894 100644 --- a/src/NodeTypeResolver/Reflection/BetterReflection/SourceLocatorProvider/DynamicSourceLocatorProvider.php +++ b/src/NodeTypeResolver/Reflection/BetterReflection/SourceLocatorProvider/DynamicSourceLocatorProvider.php @@ -3,11 +3,17 @@ declare (strict_types=1); namespace Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocatorProvider; +use PHPStan\BetterReflection\Identifier\IdentifierType; +use PHPStan\BetterReflection\Reflector\DefaultReflector; use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\File\CouldNotReadFileException; use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; +use PHPStan\Reflection\BetterReflection\SourceLocator\NewOptimizedDirectorySourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorFactory; use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocator; +use PHPStan\Reflection\ReflectionProvider; use Rector\Contract\DependencyInjection\ResetableInterface; use Rector\Testing\PHPUnit\StaticPHPUnitEnvironment; /** @@ -37,11 +43,19 @@ final class DynamicSourceLocatorProvider implements ResetableInterface * @var \PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator|null */ private $aggregateSourceLocator; + /** + * @var \PHPStan\Reflection\ReflectionProvider + */ + private $reflectionProvider; public function __construct(FileNodesFetcher $fileNodesFetcher, OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory) { $this->fileNodesFetcher = $fileNodesFetcher; $this->optimizedDirectorySourceLocatorFactory = $optimizedDirectorySourceLocatorFactory; } + public function autowire(ReflectionProvider $reflectionProvider) : void + { + $this->reflectionProvider = $reflectionProvider; + } public function setFilePath(string $filePath) : void { $this->filePaths = [$filePath]; @@ -74,8 +88,9 @@ final class DynamicSourceLocatorProvider implements ResetableInterface foreach ($this->directories as $directory) { $sourceLocators[] = $this->optimizedDirectorySourceLocatorFactory->createByDirectory($directory); } - $this->aggregateSourceLocator = new AggregateSourceLocator($sourceLocators); - return $this->aggregateSourceLocator; + $aggregateSourceLocator = $this->aggregateSourceLocator = new AggregateSourceLocator($sourceLocators); + $this->collectClasses($aggregateSourceLocator, $sourceLocators); + return $aggregateSourceLocator; } public function isPathsEmpty() : bool { @@ -90,4 +105,33 @@ final class DynamicSourceLocatorProvider implements ResetableInterface $this->directories = []; $this->aggregateSourceLocator = null; } + /** + * @param OptimizedSingleFileSourceLocator[]|NewOptimizedDirectorySourceLocator[] $sourceLocators + */ + private function collectClasses(AggregateSourceLocator $aggregateSourceLocator, array $sourceLocators) : void + { + if ($sourceLocators === []) { + return; + } + // no need to collect classes on single file, will auto collected + if (\count($sourceLocators) === 1 && $sourceLocators[0] instanceof OptimizedSingleFileSourceLocator) { + return; + } + $reflector = new DefaultReflector($aggregateSourceLocator); + $identifierClass = new IdentifierType(IdentifierType::IDENTIFIER_CLASS); + foreach ($sourceLocators as $sourceLocator) { + // trigger collect "classes" on get class on locate identifier + try { + $reflections = $sourceLocator->locateIdentifiersByType($reflector, $identifierClass); + foreach ($reflections as $reflection) { + // make 'classes' collection + try { + $this->reflectionProvider->getClass($reflection->getName()); + } catch (ClassNotFoundException $exception) { + } + } + } catch (CouldNotReadFileException $exception) { + } + } + } } diff --git a/src/VendorLocker/NodeVendorLocker/ClassMethodReturnTypeOverrideGuard.php b/src/VendorLocker/NodeVendorLocker/ClassMethodReturnTypeOverrideGuard.php index 6dd02d806e3..8467117c9b1 100644 --- a/src/VendorLocker/NodeVendorLocker/ClassMethodReturnTypeOverrideGuard.php +++ b/src/VendorLocker/NodeVendorLocker/ClassMethodReturnTypeOverrideGuard.php @@ -3,24 +3,40 @@ declare (strict_types=1); namespace Rector\VendorLocker\NodeVendorLocker; +use PhpParser\Node; use PhpParser\Node\Stmt\ClassMethod; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Type\MixedType; +use PHPStan\Type\Type; +use Rector\FamilyTree\Reflection\FamilyRelationsAnalyzer; use Rector\FileSystem\FilePathHelper; use Rector\NodeAnalyzer\MagicClassMethodAnalyzer; use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; use Rector\Reflection\ReflectionResolver; +use Rector\TypeDeclaration\TypeInferer\ReturnTypeInferer; use Rector\VendorLocker\ParentClassMethodTypeOverrideGuard; final class ClassMethodReturnTypeOverrideGuard { + /** + * @readonly + * @var \Rector\FamilyTree\Reflection\FamilyRelationsAnalyzer + */ + private $familyRelationsAnalyzer; /** * @readonly * @var \Rector\Reflection\ReflectionResolver */ private $reflectionResolver; + /** + * @readonly + * @var \Rector\TypeDeclaration\TypeInferer\ReturnTypeInferer + */ + private $returnTypeInferer; /** * @readonly * @var \Rector\VendorLocker\ParentClassMethodTypeOverrideGuard @@ -36,9 +52,11 @@ final class ClassMethodReturnTypeOverrideGuard * @var \Rector\NodeAnalyzer\MagicClassMethodAnalyzer */ private $magicClassMethodAnalyzer; - public function __construct(ReflectionResolver $reflectionResolver, ParentClassMethodTypeOverrideGuard $parentClassMethodTypeOverrideGuard, FilePathHelper $filePathHelper, MagicClassMethodAnalyzer $magicClassMethodAnalyzer) + public function __construct(FamilyRelationsAnalyzer $familyRelationsAnalyzer, ReflectionResolver $reflectionResolver, ReturnTypeInferer $returnTypeInferer, ParentClassMethodTypeOverrideGuard $parentClassMethodTypeOverrideGuard, FilePathHelper $filePathHelper, MagicClassMethodAnalyzer $magicClassMethodAnalyzer) { + $this->familyRelationsAnalyzer = $familyRelationsAnalyzer; $this->reflectionResolver = $reflectionResolver; + $this->returnTypeInferer = $returnTypeInferer; $this->parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->filePathHelper = $filePathHelper; $this->magicClassMethodAnalyzer = $magicClassMethodAnalyzer; @@ -65,7 +83,37 @@ final class ClassMethodReturnTypeOverrideGuard if (!$this->isReturnTypeChangeAllowed($classMethod, $scope)) { return \true; } - return $classMethod->isFinal(); + if ($classMethod->isFinal()) { + return \false; + } + $childrenClassReflections = $this->familyRelationsAnalyzer->getChildrenOfClassReflection($classReflection); + if ($childrenClassReflections === []) { + return \false; + } + if ($classMethod->returnType instanceof Node) { + return \true; + } + $returnType = $this->returnTypeInferer->inferFunctionLike($classMethod); + return $this->hasChildrenDifferentTypeClassMethod($classMethod, $childrenClassReflections, $returnType); + } + /** + * @param ClassReflection[] $childrenClassReflections + */ + private function hasChildrenDifferentTypeClassMethod(ClassMethod $classMethod, array $childrenClassReflections, Type $returnType) : bool + { + $methodName = $classMethod->name->toString(); + foreach ($childrenClassReflections as $childClassReflection) { + $methodReflection = $childClassReflection->getNativeMethod($methodName); + if (!$methodReflection instanceof PhpMethodReflection) { + continue; + } + $parametersAcceptor = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants()); + $childReturnType = $parametersAcceptor->getNativeReturnType(); + if (!$returnType->isSuperTypeOf($childReturnType)->yes()) { + return \true; + } + } + return \false; } private function isReturnTypeChangeAllowed(ClassMethod $classMethod, Scope $scope) : bool {