Skip to content

Commit

Permalink
Doctrine: precise Doctrine\Common\EventSubscriber detection (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
janedbal authored Dec 23, 2024
1 parent bef7d58 commit c9602b5
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 12 deletions.
126 changes: 115 additions & 11 deletions src/Provider/DoctrineUsageProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ClassMethodUsage>
*/
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<ClassMethodUsage>
*/
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')
Expand Down Expand Up @@ -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,
),
);
}

}
2 changes: 1 addition & 1 deletion tests/Rule/data/providers/doctrine.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ public function getSubscribedEvents() {
}

public function someMethod(): void {}
public function someMethod2(): void {}
public function someMethod2(): void {} // error: Unused Doctrine\MySubscriber::someMethod2

}

0 comments on commit c9602b5

Please sign in to comment.