Skip to content

Commit feb3c03

Browse files
authored
Merge pull request #2098 from Haehnchen/feature/attribute-php
support completion, navigation and method existing inspection support for "AsEventListener" attribute
2 parents 57e6b5f + 52675e3 commit feb3c03

File tree

7 files changed

+196
-17
lines changed

7 files changed

+196
-17
lines changed

src/main/java/fr/adrienbrault/idea/symfony2plugin/config/php/PhpEventDispatcherGotoCompletionRegistrar.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
import com.intellij.psi.util.PsiTreeUtil;
88
import com.jetbrains.php.lang.PhpLanguage;
99
import com.jetbrains.php.lang.parser.PhpElementTypes;
10-
import com.jetbrains.php.lang.psi.elements.Method;
11-
import com.jetbrains.php.lang.psi.elements.PhpClass;
12-
import com.jetbrains.php.lang.psi.elements.PhpReturn;
13-
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
10+
import com.jetbrains.php.lang.psi.elements.*;
1411
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider;
1512
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
1613
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
@@ -38,14 +35,12 @@ public class PhpEventDispatcherGotoCompletionRegistrar implements GotoCompletion
3835
*
3936
*/
4037
public void register(@NotNull GotoCompletionRegistrarParameter registrar) {
41-
4238
registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> {
43-
PsiElement parent = psiElement.getParent();
44-
if(!(parent instanceof StringLiteralExpression)) {
39+
if(!(psiElement.getParent() instanceof StringLiteralExpression stringLiteralExpression)) {
4540
return null;
4641
}
4742

48-
PsiElement arrayValue = parent.getParent();
43+
PsiElement arrayValue = stringLiteralExpression.getParent();
4944
if(arrayValue != null && arrayValue.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
5045
PhpReturn phpReturn = PsiTreeUtil.getParentOfType(arrayValue, PhpReturn.class);
5146
if(phpReturn != null) {
@@ -65,8 +60,21 @@ public void register(@NotNull GotoCompletionRegistrarParameter registrar) {
6560
return null;
6661
});
6762

68-
}
63+
registrar.register(PhpElementsUtil.getAttributeNamedArgumentStringPattern("\\Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener", "method"), psiElement -> {
64+
if(!(psiElement.getParent() instanceof StringLiteralExpression stringLiteralExpression)) {
65+
return null;
66+
}
6967

68+
if (stringLiteralExpression.getParent() instanceof ParameterList parameterList) {
69+
PhpAttribute parentOfType = PsiTreeUtil.getParentOfType(parameterList, PhpAttribute.class);
70+
if (parentOfType.getOwner() instanceof PhpClass phpClass) {
71+
return new PhpClassPublicMethodProvider(phpClass);
72+
}
73+
}
74+
75+
return null;
76+
});
77+
}
7078

7179
private static class PhpClassPublicMethodProvider extends GotoCompletionProvider {
7280

src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/inspection/EventMethodCallInspection.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@
1414
import com.intellij.psi.util.PsiTreeUtil;
1515
import com.jetbrains.php.lang.PhpLanguage;
1616
import com.jetbrains.php.lang.parser.PhpElementTypes;
17-
import com.jetbrains.php.lang.psi.elements.Method;
18-
import com.jetbrains.php.lang.psi.elements.PhpClass;
19-
import com.jetbrains.php.lang.psi.elements.PhpReturn;
20-
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
17+
import com.jetbrains.php.lang.psi.elements.*;
2118
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
2219
import fr.adrienbrault.idea.symfony2plugin.codeInspection.quickfix.CreateMethodQuickFix;
2320
import fr.adrienbrault.idea.symfony2plugin.config.EventDispatcherSubscriberUtil;
@@ -221,11 +218,11 @@ private String importIfNecessary(@NotNull PhpClass phpClass, String fqn) {
221218
*
222219
*/
223220
private static void visitPhpElement(@NotNull StringLiteralExpression element, @NotNull ProblemsHolder holder) {
224-
PsiElement arrayValue = element.getParent();
225-
if (arrayValue != null && arrayValue.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
226-
PhpReturn phpReturn = PsiTreeUtil.getParentOfType(arrayValue, PhpReturn.class);
221+
PsiElement parent = element.getParent();
222+
if (parent != null && parent.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
223+
PhpReturn phpReturn = PsiTreeUtil.getParentOfType(parent, PhpReturn.class);
227224
if (phpReturn != null) {
228-
Method method = PsiTreeUtil.getParentOfType(arrayValue, Method.class);
225+
Method method = PsiTreeUtil.getParentOfType(parent, Method.class);
229226
if (method != null) {
230227
String name = method.getName();
231228
if ("getSubscribedEvents".equals(name)) {
@@ -240,5 +237,15 @@ private static void visitPhpElement(@NotNull StringLiteralExpression element, @N
240237
}
241238
}
242239
}
240+
241+
if (parent instanceof ParameterList parameterList && PhpElementsUtil.isAttributeNamedArgumentString(element, "method", "\\Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener")) {
242+
PhpAttribute parentOfType = PsiTreeUtil.getParentOfType(parameterList, PhpAttribute.class);
243+
if (parentOfType.getOwner() instanceof PhpClass phpClass) {
244+
String contents = element.getContents();
245+
if (!contents.isBlank() && phpClass.findMethodByName(contents) == null) {
246+
registerMethodProblem(element, holder, phpClass);
247+
}
248+
}
249+
}
243250
}
244251
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,31 @@ public static String insertUseIfNecessary(@NotNull PsiElement phpClass, @NotNull
16871687
return null;
16881688
}
16891689

1690+
/**
1691+
* #[AsEventListener(method:'onBarEvent')]
1692+
*/
1693+
public static boolean isAttributeNamedArgumentString(@NotNull StringLiteralExpression element, @NotNull String namedArgument, @NotNull String fqn) {
1694+
PsiElement colon = PsiTreeUtil.prevCodeLeaf(element);
1695+
if (colon == null || colon.getNode().getElementType() != PhpTokenTypes.opCOLON) {
1696+
return false;
1697+
}
1698+
1699+
PsiElement argumentName = PsiTreeUtil.prevCodeLeaf(colon);
1700+
if (argumentName == null || argumentName.getNode().getElementType() != PhpTokenTypes.IDENTIFIER || !namedArgument.equals(argumentName.getText())) {
1701+
return false;
1702+
}
1703+
1704+
1705+
if (element.getParent() instanceof ParameterList parameterList) {
1706+
if (parameterList.getParent() instanceof PhpAttribute phpAttribute) {
1707+
String attributeFqn = phpAttribute.getFQN();
1708+
return fqn.equals(attributeFqn);
1709+
}
1710+
}
1711+
1712+
return false;
1713+
}
1714+
16901715
/**
16911716
* Collects all variables in a given scope.
16921717
* Eg find all variables usages in a given method
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.config.php;
2+
3+
import com.intellij.patterns.PlatformPatterns;
4+
import com.jetbrains.php.lang.psi.elements.Method;
5+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
6+
7+
/**
8+
* @author Daniel Espendiller <daniel@espendiller.net>
9+
*/
10+
public class PhpEventDispatcherGotoCompletionRegistrarTest extends SymfonyLightCodeInsightFixtureTestCase {
11+
public void setUp() throws Exception {
12+
super.setUp();
13+
myFixture.copyFileToProject("PhpEventDispatcherGotoCompletionRegistrar.php");
14+
}
15+
16+
protected String getTestDataPath() {
17+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/php/fixtures";
18+
}
19+
20+
public void testGetSubscribedEventsForMethodArrayReturn() {
21+
assertCompletionContains("test.php", "<?php\n" +
22+
"namespace App\\EventSubscriber;\n" +
23+
"\n" +
24+
"use Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;\n" +
25+
"\n" +
26+
"class ExceptionSubscriber implements EventSubscriberInterface\n" +
27+
"{\n" +
28+
" public static function getSubscribedEvents(): array\n" +
29+
" {\n" +
30+
" return [\n" +
31+
" KernelEvents::EXCEPTION => [\n" +
32+
" ['<caret>', 10],\n" +
33+
" ],\n" +
34+
" ];\n" +
35+
" }\n" +
36+
" public function processException(ExceptionEvent $event)\n" +
37+
" {\n" +
38+
" }\n" +
39+
"\n" +
40+
"}",
41+
"processException"
42+
);
43+
}
44+
45+
public void testCompletionNavigationForAsEventListenerMethodNamedArgument() {
46+
assertCompletionContains("test.php", "<?php\n" +
47+
"namespace App\\EventListener;\n" +
48+
"\n" +
49+
"use Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener;\n" +
50+
"\n" +
51+
"#[AsEventListener(event: 'bar', method: '<caret>')]\n" +
52+
"final class MyMultiListener\n" +
53+
"{\n" +
54+
" public function onFoo(): void\n" +
55+
" {\n" +
56+
" }\n" +
57+
"\n" +
58+
"}",
59+
"onFoo"
60+
);
61+
62+
assertNavigationMatch("test.php", "<?php\n" +
63+
"namespace App\\EventListener;\n" +
64+
"\n" +
65+
"use Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener;\n" +
66+
"\n" +
67+
"#[AsEventListener(event: 'bar', method: 'onF<caret>oo')]\n" +
68+
"final class MyMultiListener\n" +
69+
"{\n" +
70+
" public function onFoo(): void\n" +
71+
" {\n" +
72+
" }\n" +
73+
"\n" +
74+
"}", PlatformPatterns.psiElement(Method.class)
75+
);
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Symfony\Component\EventDispatcher\Attribute
4+
{
5+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
6+
class AsEventListener
7+
{
8+
public function __construct(
9+
public ?string $event = null,
10+
public ?string $method = null,
11+
public int $priority = 0,
12+
public ?string $dispatcher = null,
13+
) {
14+
}
15+
}
16+
}
17+
18+
namespace Symfony\Component\EventDispatcher
19+
{
20+
interface EventSubscriberInterface {}
21+
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/inspection/EventMethodCallInspectionTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,31 @@ public void testThatXmlCallsProvidesMethodExistsCheck() {
9696
"Missing Method"
9797
);
9898
}
99+
100+
public void testThatPhpCallsProvidesMethodExistsForPhpAttributeCheck() {
101+
assertLocalInspectionContains("test.php", "<?php\n" +
102+
"use Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener;\n" +
103+
"\n" +
104+
"#[AsEventListener(event: CustomEvent::class, method: 'onF<caret>ooBar')]\n" +
105+
"final class MyMultiListener\n" +
106+
"{\n" +
107+
"\n" +
108+
"}",
109+
"Missing Method"
110+
);
111+
112+
assertLocalInspectionNotContains("test.php", "<?php\n" +
113+
"use Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener;\n" +
114+
"\n" +
115+
"#[AsEventListener(event: CustomEvent::class, method: 'on<caret>Foo')]\n" +
116+
"final class MyMultiListener\n" +
117+
"{\n" +
118+
" public static function onFoo()\n" +
119+
" {\n" +
120+
" }\n" +
121+
"\n" +
122+
"}",
123+
"Missing Method"
124+
);
125+
}
99126
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/inspection/fixtures/classes.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,17 @@ public function getFoo() {}
1313
interface EventSubscriberInterface {}
1414
}
1515

16+
namespace Symfony\Component\EventDispatcher\Attribute
17+
{
18+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
19+
class AsEventListener
20+
{
21+
public function __construct(
22+
public ?string $event = null,
23+
public ?string $method = null,
24+
public int $priority = 0,
25+
public ?string $dispatcher = null,
26+
) {
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)