Skip to content

Commit f006be7

Browse files
authored
Symfony & Doctrine: improve provider extendability (#121)
1 parent 74af80b commit f006be7

File tree

4 files changed

+106
-41
lines changed

4 files changed

+106
-41
lines changed

src/Provider/DoctrineUsageProvider.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,35 @@ public function shouldMarkMethodAsUsed(ReflectionMethod $method): bool
2626
$methodName = $method->getName();
2727
$class = $method->getDeclaringClass();
2828

29-
return $class->implementsInterface('Doctrine\Common\EventSubscriber')
30-
|| $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PostLoad')
29+
return $this->isEventSubscriberMethod($method)
30+
|| $this->isLifecycleEventMethod($method)
31+
|| $this->isEntityRepositoryConstructor($class, $method)
32+
|| $this->isPartOfAsEntityListener($class, $methodName)
33+
|| $this->isProbablyDoctrineListener($methodName);
34+
}
35+
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+
42+
protected function isLifecycleEventMethod(ReflectionMethod $method): bool
43+
{
44+
return $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PostLoad')
3145
|| $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PostPersist')
3246
|| $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PostUpdate')
3347
|| $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PreFlush')
3448
|| $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PrePersist')
3549
|| $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PreRemove')
36-
|| $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PreUpdate')
37-
|| $this->isEntityRepositoryConstructor($class, $method)
38-
|| $this->isPartOfAsEntityListener($class, $methodName)
39-
|| $this->isProbablyDoctrineListener($methodName);
50+
|| $this->hasAttribute($method, 'Doctrine\ORM\Mapping\PreUpdate');
4051
}
4152

4253
/**
4354
* Ideally, we would need to parse DIC xml to know this for sure just like phpstan-symfony does.
4455
* - see Doctrine\ORM\Events::*
4556
*/
46-
private function isProbablyDoctrineListener(string $methodName): bool
57+
protected function isProbablyDoctrineListener(string $methodName): bool
4758
{
4859
return $methodName === 'preRemove'
4960
|| $methodName === 'postRemove'
@@ -60,7 +71,7 @@ private function isProbablyDoctrineListener(string $methodName): bool
6071
|| $methodName === 'onClear';
6172
}
6273

63-
private function hasAttribute(ReflectionMethod $method, string $attributeClass): bool
74+
protected function hasAttribute(ReflectionMethod $method, string $attributeClass): bool
6475
{
6576
if (PHP_VERSION_ID < 8_00_00) {
6677
return false;
@@ -72,7 +83,7 @@ private function hasAttribute(ReflectionMethod $method, string $attributeClass):
7283
/**
7384
* @param ReflectionClass<object> $class
7485
*/
75-
private function isPartOfAsEntityListener(ReflectionClass $class, string $methodName): bool
86+
protected function isPartOfAsEntityListener(ReflectionClass $class, string $methodName): bool
7687
{
7788
if (PHP_VERSION_ID < 8_00_00) {
7889
return false;
@@ -92,7 +103,7 @@ private function isPartOfAsEntityListener(ReflectionClass $class, string $method
92103
/**
93104
* @param ReflectionClass<object> $class
94105
*/
95-
private function isEntityRepositoryConstructor(ReflectionClass $class, ReflectionMethod $method): bool
106+
protected function isEntityRepositoryConstructor(ReflectionClass $class, ReflectionMethod $method): bool
96107
{
97108
if (!$method->isConstructor()) {
98109
return false;

src/Provider/SymfonyUsageProvider.php

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,6 @@ public function __construct(
3939
}
4040
}
4141

42-
private function fillDicClasses(ServiceMapFactory $serviceMapFactory): void
43-
{
44-
foreach ($serviceMapFactory->create()->getServices() as $service) { // @phpstan-ignore phpstanApi.method
45-
$dicClass = $service->getClass();
46-
47-
if ($dicClass === null) {
48-
continue;
49-
}
50-
51-
$this->dicClasses[$dicClass] = true;
52-
}
53-
}
54-
5542
public function getUsages(Node $node, Scope $scope): array
5643
{
5744
if (!$this->enabled || !$node instanceof InClassNode) { // @phpstan-ignore phpstanApi.instanceofAssumption
@@ -81,28 +68,80 @@ public function getUsages(Node $node, Scope $scope): array
8168
return $usages;
8269
}
8370

84-
private function shouldMarkAsUsed(ReflectionMethod $method): bool
71+
protected function shouldMarkAsUsed(ReflectionMethod $method): bool
72+
{
73+
return $this->isEventSubscriberMethod($method)
74+
|| $this->isBundleConstructor($method)
75+
|| $this->isEventListenerMethodWithAsEventListenerAttribute($method)
76+
|| $this->isAutowiredWithRequiredAttribute($method)
77+
|| $this->isConstructorWithAsCommandAttribute($method)
78+
|| $this->isConstructorWithAsControllerAttribute($method)
79+
|| $this->isMethodWithRouteAttribute($method)
80+
|| $this->isProbablySymfonyListener($method);
81+
}
82+
83+
protected function fillDicClasses(ServiceMapFactory $serviceMapFactory): void
84+
{
85+
foreach ($serviceMapFactory->create()->getServices() as $service) { // @phpstan-ignore phpstanApi.method
86+
$dicClass = $service->getClass();
87+
88+
if ($dicClass === null) {
89+
continue;
90+
}
91+
92+
$this->dicClasses[$dicClass] = true;
93+
}
94+
}
95+
96+
protected function isEventSubscriberMethod(ReflectionMethod $method): bool
97+
{
98+
// this is simplification, we should deduce that from AST of getSubscribedEvents() method
99+
return $method->getDeclaringClass()->implementsInterface('Symfony\Component\EventDispatcher\EventSubscriberInterface');
100+
}
101+
102+
protected function isBundleConstructor(ReflectionMethod $method): bool
103+
{
104+
return $method->isConstructor() && $method->getDeclaringClass()->isSubclassOf('Symfony\Component\HttpKernel\Bundle\Bundle');
105+
}
106+
107+
protected function isAutowiredWithRequiredAttribute(ReflectionMethod $method): bool
108+
{
109+
return $this->hasAttribute($method, 'Symfony\Contracts\Service\Attribute\Required');
110+
}
111+
112+
protected function isEventListenerMethodWithAsEventListenerAttribute(ReflectionMethod $method): bool
85113
{
86-
$methodName = $method->getName();
87114
$class = $method->getDeclaringClass();
88115

89-
return $class->implementsInterface('Symfony\Component\EventDispatcher\EventSubscriberInterface')
90-
|| ($class->isSubclassOf('Symfony\Component\HttpKernel\Bundle\Bundle') && $method->isConstructor())
91-
|| $this->hasAttribute($class, 'Symfony\Component\EventDispatcher\Attribute\AsEventListener')
92-
|| $this->hasAttribute($method, 'Symfony\Component\EventDispatcher\Attribute\AsEventListener')
93-
|| $this->hasAttribute($method, 'Symfony\Contracts\Service\Attribute\Required')
94-
|| ($this->hasAttribute($class, 'Symfony\Component\Console\Attribute\AsCommand') && $method->isConstructor())
95-
|| ($this->hasAttribute($class, 'Symfony\Component\HttpKernel\Attribute\AsController') && $method->isConstructor())
96-
|| $this->hasAttribute($method, 'Symfony\Component\Routing\Attribute\Route', ReflectionAttribute::IS_INSTANCEOF)
97-
|| $this->hasAttribute($method, 'Symfony\Component\Routing\Annotation\Route', ReflectionAttribute::IS_INSTANCEOF)
98-
|| $this->isProbablySymfonyListener($methodName);
116+
return $this->hasAttribute($class, 'Symfony\Component\EventDispatcher\Attribute\AsEventListener')
117+
|| $this->hasAttribute($method, 'Symfony\Component\EventDispatcher\Attribute\AsEventListener');
118+
}
119+
120+
protected function isConstructorWithAsCommandAttribute(ReflectionMethod $method): bool
121+
{
122+
$class = $method->getDeclaringClass();
123+
return $method->isConstructor() && $this->hasAttribute($class, 'Symfony\Component\Console\Attribute\AsCommand');
124+
}
125+
126+
protected function isConstructorWithAsControllerAttribute(ReflectionMethod $method): bool
127+
{
128+
$class = $method->getDeclaringClass();
129+
return $method->isConstructor() && $this->hasAttribute($class, 'Symfony\Component\HttpKernel\Attribute\AsController');
130+
}
131+
132+
protected function isMethodWithRouteAttribute(ReflectionMethod $method): bool
133+
{
134+
return $this->hasAttribute($method, 'Symfony\Component\Routing\Attribute\Route', ReflectionAttribute::IS_INSTANCEOF)
135+
|| $this->hasAttribute($method, 'Symfony\Component\Routing\Annotation\Route', ReflectionAttribute::IS_INSTANCEOF);
99136
}
100137

101138
/**
102139
* Ideally, we would need to parse DIC xml to know this for sure just like phpstan-symfony does.
103140
*/
104-
private function isProbablySymfonyListener(string $methodName): bool
141+
protected function isProbablySymfonyListener(ReflectionMethod $method): bool
105142
{
143+
$methodName = $method->getName();
144+
106145
return $methodName === 'onKernelResponse'
107146
|| $methodName === 'onKernelException'
108147
|| $methodName === 'onKernelRequest'
@@ -116,7 +155,7 @@ private function isProbablySymfonyListener(string $methodName): bool
116155
* @param ReflectionClass<object>|ReflectionMethod $classOrMethod
117156
* @param ReflectionAttribute::IS_*|0 $flags
118157
*/
119-
private function hasAttribute(Reflector $classOrMethod, string $attributeClass, int $flags = 0): bool
158+
protected function hasAttribute(Reflector $classOrMethod, string $attributeClass, int $flags = 0): bool
120159
{
121160
if (PHP_VERSION_ID < 8_00_00) {
122161
return false;
@@ -143,13 +182,13 @@ private function isSymfonyInstalled(): bool
143182
|| InstalledVersions::isInstalled('symfony/http-kernel');
144183
}
145184

146-
private function createUsage(ExtendedMethodReflection $getNativeMethod): ClassMethodUsage
185+
private function createUsage(ExtendedMethodReflection $methodReflection): ClassMethodUsage
147186
{
148187
return new ClassMethodUsage(
149188
null,
150189
new ClassMethodRef(
151-
$getNativeMethod->getDeclaringClass()->getName(),
152-
$getNativeMethod->getName(),
190+
$methodReflection->getDeclaringClass()->getName(),
191+
$methodReflection->getName(),
153192
false,
154193
),
155194
);

tests/Rule/data/providers/doctrine.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ public function deadCode(): void // error: Unused Doctrine\OldListenerHeuristics
5151
class MySubscriber implements \Doctrine\Common\EventSubscriber {
5252

5353
public function getSubscribedEvents() {
54-
return [];
54+
return [
55+
'someMethod',
56+
];
5557
}
5658

59+
public function someMethod(): void {}
60+
public function someMethod2(): void {}
61+
5762
}

tests/Rule/data/providers/symfony.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ public function onKernelRequest(): void
5151
{
5252
}
5353

54+
public function onNonsense(): void
55+
{
56+
}
57+
5458
public static function getSubscribedEvents(): array
5559
{
5660
return [
@@ -66,6 +70,12 @@ class HelloController
6670
public function __construct() {}
6771
}
6872

73+
#[AsCommand('name')]
74+
class HelloCommand
75+
{
76+
public function __construct() {}
77+
}
78+
6979
class DicClassParent { // not present in DIC, but ctor is not dead
7080
public function __construct() {}
7181
}

0 commit comments

Comments
 (0)