Skip to content

Commit 593829a

Browse files
authored
Symfony: precise detection of EventSubscriberInterface::getSubscribedEvents (#122)
1 parent f006be7 commit 593829a

File tree

2 files changed

+124
-12
lines changed

2 files changed

+124
-12
lines changed

src/Provider/SymfonyUsageProvider.php

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
use Composer\InstalledVersions;
66
use PhpParser\Node;
7+
use PhpParser\Node\Stmt\Return_;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
910
use PHPStan\Node\InClassNode;
1011
use PHPStan\Reflection\ExtendedMethodReflection;
12+
use PHPStan\Reflection\MethodReflection;
1113
use PHPStan\Symfony\ServiceMapFactory;
1214
use ReflectionAttribute;
1315
use ReflectionClass;
@@ -41,10 +43,115 @@ public function __construct(
4143

4244
public function getUsages(Node $node, Scope $scope): array
4345
{
44-
if (!$this->enabled || !$node instanceof InClassNode) { // @phpstan-ignore phpstanApi.instanceofAssumption
46+
if (!$this->enabled) {
4547
return [];
4648
}
4749

50+
$usages = [];
51+
52+
if ($node instanceof InClassNode) { // @phpstan-ignore phpstanApi.instanceofAssumption
53+
$usages = [
54+
...$usages,
55+
...$this->getUsagesFromReflection($node),
56+
];
57+
}
58+
59+
if ($node instanceof Return_) {
60+
$usages = [
61+
...$usages,
62+
...$this->getUsagesOfEventSubscriber($node, $scope),
63+
];
64+
}
65+
66+
return $usages;
67+
}
68+
69+
/**
70+
* @return list<ClassMethodUsage>
71+
*/
72+
private function getUsagesOfEventSubscriber(Return_ $node, Scope $scope): array
73+
{
74+
if ($node->expr === null) {
75+
return [];
76+
}
77+
78+
if (!$scope->isInClass()) {
79+
return [];
80+
}
81+
82+
if (!$scope->getFunction() instanceof MethodReflection) {
83+
return [];
84+
}
85+
86+
if ($scope->getFunction()->getName() !== 'getSubscribedEvents') {
87+
return [];
88+
}
89+
90+
if (!$scope->getClassReflection()->implementsInterface('Symfony\Component\EventDispatcher\EventSubscriberInterface')) {
91+
return [];
92+
}
93+
94+
$className = $scope->getClassReflection()->getName();
95+
96+
$usages = [];
97+
98+
// phpcs:disable Squiz.PHP.CommentedOutCode.Found
99+
foreach ($scope->getType($node->expr)->getConstantArrays() as $rootArray) {
100+
foreach ($rootArray->getValuesArray()->getValueTypes() as $eventConfig) {
101+
// ['eventName' => 'methodName']
102+
foreach ($eventConfig->getConstantStrings() as $subscriberMethodString) {
103+
$usages[] = new ClassMethodUsage(
104+
null,
105+
new ClassMethodRef(
106+
$className,
107+
$subscriberMethodString->getValue(),
108+
true,
109+
),
110+
);
111+
}
112+
113+
// ['eventName' => ['methodName', $priority]]
114+
foreach ($eventConfig->getConstantArrays() as $subscriberMethodArray) {
115+
foreach ($subscriberMethodArray->getFirstIterableValueType()->getConstantStrings() as $subscriberMethodString) {
116+
$usages[] = new ClassMethodUsage(
117+
null,
118+
new ClassMethodRef(
119+
$className,
120+
$subscriberMethodString->getValue(),
121+
true,
122+
),
123+
);
124+
}
125+
}
126+
127+
// ['eventName' => [['methodName', $priority], ['methodName', $priority]]]
128+
foreach ($eventConfig->getConstantArrays() as $subscriberMethodArray) {
129+
foreach ($subscriberMethodArray->getIterableValueType()->getConstantArrays() as $innerArray) {
130+
foreach ($innerArray->getFirstIterableValueType()->getConstantStrings() as $subscriberMethodString) {
131+
$usages[] = new ClassMethodUsage(
132+
null,
133+
new ClassMethodRef(
134+
$className,
135+
$subscriberMethodString->getValue(),
136+
true,
137+
),
138+
);
139+
}
140+
}
141+
}
142+
}
143+
}
144+
145+
// phpcs:enable // phpcs:disable Squiz.PHP.CommentedOutCode.Found
146+
147+
return $usages;
148+
}
149+
150+
/**
151+
* @return list<ClassMethodUsage>
152+
*/
153+
private function getUsagesFromReflection(InClassNode $node): array
154+
{
48155
$classReflection = $node->getClassReflection();
49156
$nativeReflection = $classReflection->getNativeReflection();
50157
$className = $classReflection->getName();
@@ -70,8 +177,7 @@ public function getUsages(Node $node, Scope $scope): array
70177

71178
protected function shouldMarkAsUsed(ReflectionMethod $method): bool
72179
{
73-
return $this->isEventSubscriberMethod($method)
74-
|| $this->isBundleConstructor($method)
180+
return $this->isBundleConstructor($method)
75181
|| $this->isEventListenerMethodWithAsEventListenerAttribute($method)
76182
|| $this->isAutowiredWithRequiredAttribute($method)
77183
|| $this->isConstructorWithAsCommandAttribute($method)
@@ -93,12 +199,6 @@ protected function fillDicClasses(ServiceMapFactory $serviceMapFactory): void
93199
}
94200
}
95201

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-
102202
protected function isBundleConstructor(ReflectionMethod $method): bool
103203
{
104204
return $method->isConstructor() && $method->getDeclaringClass()->isSubclassOf('Symfony\Component\HttpKernel\Bundle\Bundle');

tests/Rule/data/providers/symfony.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1010
use Symfony\Component\HttpKernel\Attribute\AsController;
1111
use Symfony\Component\HttpKernel\Bundle\Bundle;
12+
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
13+
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
1214
use Symfony\Component\Routing\Attribute\Route;
1315
use Symfony\Contracts\Service\Attribute\Required;
1416

@@ -47,18 +49,28 @@ public function __construct() {
4749
class SomeSubscriber implements EventSubscriberInterface
4850
{
4951

50-
public function onKernelRequest(): void
52+
public function string(): void
5153
{
5254
}
5355

54-
public function onNonsense(): void
56+
public function stringInArray(): void
57+
{
58+
}
59+
60+
public function stringInArrayArray(): void
61+
{
62+
}
63+
64+
public function onNonsense(): void // error: Unused Symfony\SomeSubscriber::onNonsense
5565
{
5666
}
5767

5868
public static function getSubscribedEvents(): array
5969
{
6070
return [
61-
'kernel.request' => [['onKernelRequest', 0]],
71+
'kernel.exception' => 'string',
72+
'kernel.controller' => ['stringInArray', 1],
73+
'kernel.request' => [['stringInArrayArray', 0]],
6274
];
6375
}
6476

0 commit comments

Comments
 (0)