Skip to content

Commit c9602b5

Browse files
authored
Doctrine: precise Doctrine\Common\EventSubscriber detection (#127)
1 parent bef7d58 commit c9602b5

File tree

2 files changed

+116
-12
lines changed

2 files changed

+116
-12
lines changed

src/Provider/DoctrineUsageProvider.php

+115-11
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33
namespace ShipMonk\PHPStan\DeadCode\Provider;
44

55
use Composer\InstalledVersions;
6+
use PhpParser\Node;
7+
use PhpParser\Node\Stmt\Return_;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Node\InClassNode;
10+
use PHPStan\Reflection\ExtendedMethodReflection;
11+
use PHPStan\Reflection\MethodReflection;
612
use ReflectionClass;
713
use ReflectionMethod;
14+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodRef;
15+
use ShipMonk\PHPStan\DeadCode\Graph\ClassMethodUsage;
816
use const PHP_VERSION_ID;
917

10-
class DoctrineUsageProvider extends ReflectionBasedMemberUsageProvider
18+
class DoctrineUsageProvider implements MemberUsageProvider
1119
{
1220

1321
private bool $enabled;
@@ -17,28 +25,112 @@ public function __construct(?bool $enabled)
1725
$this->enabled = $enabled ?? $this->isDoctrineInstalled();
1826
}
1927

20-
public function shouldMarkMethodAsUsed(ReflectionMethod $method): bool
28+
public function getUsages(Node $node, Scope $scope): array
2129
{
2230
if (!$this->enabled) {
23-
return false;
31+
return [];
32+
}
33+
34+
$usages = [];
35+
36+
if ($node instanceof InClassNode) { // @phpstan-ignore phpstanApi.instanceofAssumption
37+
$usages = [
38+
...$usages,
39+
...$this->getUsagesFromReflection($node),
40+
];
41+
}
42+
43+
if ($node instanceof Return_) {
44+
$usages = [
45+
...$usages,
46+
...$this->getUsagesOfEventSubscriber($node, $scope),
47+
];
2448
}
2549

50+
return $usages;
51+
}
52+
53+
/**
54+
* @return list<ClassMethodUsage>
55+
*/
56+
private function getUsagesFromReflection(InClassNode $node): array
57+
{
58+
$classReflection = $node->getClassReflection();
59+
$nativeReflection = $classReflection->getNativeReflection();
60+
61+
$usages = [];
62+
63+
foreach ($nativeReflection->getMethods() as $method) {
64+
if ($method->getDeclaringClass()->getName() !== $nativeReflection->getName()) {
65+
continue;
66+
}
67+
68+
if ($this->shouldMarkMethodAsUsed($method)) {
69+
$usages[] = $this->createMethodUsage($classReflection->getNativeMethod($method->getName()));
70+
}
71+
}
72+
73+
return $usages;
74+
}
75+
76+
/**
77+
* @return list<ClassMethodUsage>
78+
*/
79+
private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
80+
{
81+
if ($node->expr === null) {
82+
return [];
83+
}
84+
85+
if (!$scope->isInClass()) {
86+
return [];
87+
}
88+
89+
if (!$scope->getFunction() instanceof MethodReflection) {
90+
return [];
91+
}
92+
93+
if ($scope->getFunction()->getName() !== 'getSubscribedEvents') {
94+
return [];
95+
}
96+
97+
if (!$scope->getClassReflection()->implementsInterface('Doctrine\Common\EventSubscriber')) {
98+
return [];
99+
}
100+
101+
$className = $scope->getClassReflection()->getName();
102+
103+
$usages = [];
104+
105+
foreach ($scope->getType($node->expr)->getConstantArrays() as $rootArray) {
106+
foreach ($rootArray->getValuesArray()->getValueTypes() as $eventConfig) {
107+
foreach ($eventConfig->getConstantStrings() as $subscriberMethodString) {
108+
$usages[] = new ClassMethodUsage(
109+
null,
110+
new ClassMethodRef(
111+
$className,
112+
$subscriberMethodString->getValue(),
113+
true,
114+
),
115+
);
116+
}
117+
}
118+
}
119+
120+
return $usages;
121+
}
122+
123+
protected function shouldMarkMethodAsUsed(ReflectionMethod $method): bool
124+
{
26125
$methodName = $method->getName();
27126
$class = $method->getDeclaringClass();
28127

29-
return $this->isEventSubscriberMethod($method)
30-
|| $this->isLifecycleEventMethod($method)
128+
return $this->isLifecycleEventMethod($method)
31129
|| $this->isEntityRepositoryConstructor($class, $method)
32130
|| $this->isPartOfAsEntityListener($class, $methodName)
33131
|| $this->isProbablyDoctrineListener($methodName);
34132
}
35133

36-
protected function isEventSubscriberMethod(ReflectionMethod $method): bool
37-
{
38-
// this is simplification, we should deduce that from AST of getSubscribedEvents() method
39-
return $method->getDeclaringClass()->implementsInterface('Doctrine\Common\EventSubscriber');
40-
}
41-
42134
protected function isLifecycleEventMethod(ReflectionMethod $method): bool
43135
{
44136
return $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PostLoad')
@@ -119,4 +211,16 @@ private function isDoctrineInstalled(): bool
119211
|| InstalledVersions::isInstalled('doctrine/doctrine-bundle');
120212
}
121213

214+
private function createMethodUsage(ExtendedMethodReflection $methodReflection): ClassMethodUsage
215+
{
216+
return new ClassMethodUsage(
217+
null,
218+
new ClassMethodRef(
219+
$methodReflection->getDeclaringClass()->getName(),
220+
$methodReflection->getName(),
221+
false,
222+
),
223+
);
224+
}
225+
122226
}

tests/Rule/data/providers/doctrine.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ public function getSubscribedEvents() {
5757
}
5858

5959
public function someMethod(): void {}
60-
public function someMethod2(): void {}
60+
public function someMethod2(): void {} // error: Unused Doctrine\MySubscriber::someMethod2
6161

6262
}

0 commit comments

Comments
 (0)