diff --git a/src/Provider/DoctrineUsageProvider.php b/src/Provider/DoctrineUsageProvider.php index 4b264e3..d2075db 100644 --- a/src/Provider/DoctrineUsageProvider.php +++ b/src/Provider/DoctrineUsageProvider.php @@ -3,11 +3,19 @@ namespace ShipMonk\PHPStan\DeadCode\Provider; use Composer\InstalledVersions; +use PhpParser\Node; +use PhpParser\Node\Stmt\Return_; +use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\MethodReflection; use ReflectionClass; use ReflectionMethod; +use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef; +use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage; use const PHP_VERSION_ID; -class DoctrineUsageProvider extends ReflectionBasedMemberUsageProvider +class DoctrineUsageProvider implements MemberUsageProvider { private bool $enabled; @@ -17,28 +25,112 @@ public function __construct(?bool $enabled) $this->enabled = $enabled ?? $this->isDoctrineInstalled(); } - public function shouldMarkMethodAsUsed(ReflectionMethod $method): bool + public function getUsages(Node $node, Scope $scope): array { if (!$this->enabled) { - return false; + return []; + } + + $usages = []; + + if ($node instanceof InClassNode) { // @phpstan-ignore phpstanApi.instanceofAssumption + $usages = [ + ...$usages, + ...$this->getUsagesFromReflection($node), + ]; + } + + if ($node instanceof Return_) { + $usages = [ + ...$usages, + ...$this->getUsagesOfEventSubscriber($node, $scope), + ]; } + return $usages; + } + + /** + * @return list + */ + private function getUsagesFromReflection(InClassNode $node): array + { + $classReflection = $node->getClassReflection(); + $nativeReflection = $classReflection->getNativeReflection(); + + $usages = []; + + foreach ($nativeReflection->getMethods() as $method) { + if ($method->getDeclaringClass()->getName() !== $nativeReflection->getName()) { + continue; + } + + if ($this->shouldMarkMethodAsUsed($method)) { + $usages[] = $this->createMethodUsage($classReflection->getNativeMethod($method->getName())); + } + } + + return $usages; + } + + /** + * @return list + */ + private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array + { + if ($node->expr === null) { + return []; + } + + if (!$scope->isInClass()) { + return []; + } + + if (!$scope->getFunction() instanceof MethodReflection) { + return []; + } + + if ($scope->getFunction()->getName() !== 'getSubscribedEvents') { + return []; + } + + if (!$scope->getClassReflection()->implementsInterface('Doctrine\Common\EventSubscriber')) { + return []; + } + + $className = $scope->getClassReflection()->getName(); + + $usages = []; + + foreach ($scope->getType($node->expr)->getConstantArrays() as $rootArray) { + foreach ($rootArray->getValuesArray()->getValueTypes() as $eventConfig) { + foreach ($eventConfig->getConstantStrings() as $subscriberMethodString) { + $usages[] = new ClassMethodUsage( + null, + new ClassMethodRef( + $className, + $subscriberMethodString->getValue(), + true, + ), + ); + } + } + } + + return $usages; + } + + protected function shouldMarkMethodAsUsed(ReflectionMethod $method): bool + { $methodName = $method->getName(); $class = $method->getDeclaringClass(); - return $this->isEventSubscriberMethod($method) - || $this->isLifecycleEventMethod($method) + return $this->isLifecycleEventMethod($method) || $this->isEntityRepositoryConstructor($class, $method) || $this->isPartOfAsEntityListener($class, $methodName) || $this->isProbablyDoctrineListener($methodName); } - protected function isEventSubscriberMethod(ReflectionMethod $method): bool - { - // this is simplification, we should deduce that from AST of getSubscribedEvents() method - return $method->getDeclaringClass()->implementsInterface('Doctrine\Common\EventSubscriber'); - } - protected function isLifecycleEventMethod(ReflectionMethod $method): bool { return $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PostLoad') @@ -119,4 +211,16 @@ private function isDoctrineInstalled(): bool || InstalledVersions::isInstalled('doctrine/doctrine-bundle'); } + private function createMethodUsage(ExtendedMethodReflection $methodReflection): ClassMethodUsage + { + return new ClassMethodUsage( + null, + new ClassMethodRef( + $methodReflection->getDeclaringClass()->getName(), + $methodReflection->getName(), + false, + ), + ); + } + } diff --git a/tests/Rule/data/providers/doctrine.php b/tests/Rule/data/providers/doctrine.php index 91e39ef..b24042b 100644 --- a/tests/Rule/data/providers/doctrine.php +++ b/tests/Rule/data/providers/doctrine.php @@ -57,6 +57,6 @@ public function getSubscribedEvents() { } public function someMethod(): void {} - public function someMethod2(): void {} + public function someMethod2(): void {} // error: Unused Doctrine\MySubscriber::someMethod2 }