Skip to content

Commit 5f84c9b

Browse files
authored
Merge pull request #1898 from Haehnchen/feature/1366-used
#1366 mark controller and its action as "used" code
2 parents 3f94ae1 + 846a9b8 commit 5f84c9b

File tree

7 files changed

+285
-0
lines changed

7 files changed

+285
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package fr.adrienbrault.idea.symfony2plugin.codeInsight;
2+
3+
import com.intellij.codeInsight.daemon.ImplicitUsageProvider;
4+
import com.intellij.psi.PsiElement;
5+
import com.jetbrains.php.lang.psi.elements.Method;
6+
import com.jetbrains.php.lang.psi.elements.PhpAttribute;
7+
import com.jetbrains.php.lang.psi.elements.PhpClass;
8+
import com.jetbrains.php.lang.psi.elements.PhpModifier;
9+
import de.espend.idea.php.annotation.dict.PhpDocCommentAnnotation;
10+
import de.espend.idea.php.annotation.util.AnnotationUtil;
11+
import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper;
12+
import org.jetbrains.annotations.NotNull;
13+
14+
import java.util.Collection;
15+
16+
/**
17+
* @author Daniel Espendiller <daniel@espendiller.net>
18+
*/
19+
public class SymfonyImplicitUsageProvider implements ImplicitUsageProvider {
20+
private static final String[] ROUTE_ANNOTATIONS = new String[]{
21+
"\\Symfony\\Component\\Routing\\Annotation\\Route",
22+
"\\Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Route"
23+
};
24+
25+
@Override
26+
public boolean isImplicitUsage(@NotNull PsiElement element) {
27+
if (element instanceof Method && ((Method) element).getAccess() == PhpModifier.Access.PUBLIC) {
28+
return isMethodARoute((Method) element);
29+
} else if (element instanceof PhpClass) {
30+
return ((PhpClass) element).getMethods()
31+
.stream()
32+
.filter(method -> method.getAccess() == PhpModifier.Access.PUBLIC)
33+
.anyMatch(this::isMethodARoute);
34+
}
35+
36+
return false;
37+
}
38+
39+
@Override
40+
public boolean isImplicitRead(@NotNull PsiElement element) {
41+
return false;
42+
}
43+
44+
@Override
45+
public boolean isImplicitWrite(@NotNull PsiElement element) {
46+
return false;
47+
}
48+
49+
private boolean isMethodARoute(@NotNull Method method) {
50+
PhpDocCommentAnnotation phpDocCommentAnnotationContainer = AnnotationUtil.getPhpDocCommentAnnotationContainer(method.getDocComment());
51+
if (phpDocCommentAnnotationContainer != null && phpDocCommentAnnotationContainer.getFirstPhpDocBlock(ROUTE_ANNOTATIONS) != null) {
52+
return true;
53+
}
54+
55+
for (String route : ROUTE_ANNOTATIONS) {
56+
Collection<@NotNull PhpAttribute> attributes = method.getAttributes(route);
57+
if (!attributes.isEmpty()) {
58+
return true;
59+
}
60+
}
61+
62+
return RouteHelper.isRouteExistingForMethod(method);
63+
}
64+
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
public class RouteHelper {
6363

6464
private static final Key<CachedValue<Map<String, Route>>> ROUTE_CACHE = new Key<>("SYMFONY:ROUTE_CACHE");
65+
private static final Key<CachedValue<Set<String>>> ROUTE_CONTROLLER_RESOLVED_CACHE = new Key<>("ROUTE_CONTROLLER_RESOLVED_CACHE");
66+
6567
private static final Key<CachedValue<Map<String, Route>>> SYMFONY_COMPILED_CACHE_ROUTES = new Key<>("SYMFONY_COMPILED_CACHE_ROUTES");
6668
private static final Key<CachedValue<Collection<String>>> SYMFONY_COMPILED_CACHE_ROUTES_FILES = new Key<>("SYMFONY_COMPILED_CACHE_ROUTES_FILES");
6769
private static final Key<CachedValue<Collection<String>>> SYMFONY_COMPILED_GUESTED_FILES = new Key<>("SYMFONY_COMPILED_GUESTED_FILES");
@@ -1167,6 +1169,42 @@ public static List<PsiElement> getRouteDefinitionTargets(Project project, String
11671169
return targets;
11681170
}
11691171

1172+
public static boolean isRouteExistingForMethod(final @NotNull Method method) {
1173+
Project project = method.getProject();
1174+
1175+
Set<String> cachedValue = CachedValuesManager.getManager(project).getCachedValue(
1176+
project,
1177+
ROUTE_CONTROLLER_RESOLVED_CACHE,
1178+
() -> {
1179+
Set<String> items = new HashSet<>();
1180+
1181+
for (Map.Entry<String, Route> pair : RouteHelper.getAllRoutes(project).entrySet()) {
1182+
String controller = pair.getValue().getController();
1183+
if (controller != null) {
1184+
for (PsiElement psiElement : RouteHelper.getMethodsOnControllerShortcut(project, controller)) {
1185+
if (psiElement instanceof Method) {
1186+
items.add(((Method) psiElement).getFQN());
1187+
}
1188+
}
1189+
}
1190+
}
1191+
1192+
return CachedValueProvider.Result.create(items, PsiModificationTracker.MODIFICATION_COUNT);
1193+
},
1194+
false
1195+
);
1196+
1197+
String fqn = method.getFQN();
1198+
if (fqn.toLowerCase().endsWith("action")) {
1199+
String substring = fqn.substring(0, fqn .length() - "action".length());
1200+
if (cachedValue.contains(substring)) {
1201+
return true;
1202+
}
1203+
}
1204+
1205+
return cachedValue.contains(fqn);
1206+
}
1207+
11701208
@NotNull
11711209
public static Map<String, Route> getAllRoutes(final @NotNull Project project) {
11721210
return CachedValuesManager.getManager(project).getCachedValue(

src/main/resources/META-INF/plugin.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@
244244
<gotoSymbolContributor implementation="fr.adrienbrault.idea.symfony2plugin.navigation.RouteSymbolContributor"/>
245245
<gotoSymbolContributor implementation="fr.adrienbrault.idea.symfony2plugin.navigation.SymfonyCommandSymbolContributor"/>
246246

247+
<implicitUsageProvider implementation="fr.adrienbrault.idea.symfony2plugin.codeInsight.SymfonyImplicitUsageProvider"/>
248+
247249
<gotoFileContributor implementation="fr.adrienbrault.idea.symfony2plugin.navigation.TemplateFileContributor"/>
248250

249251
<gotoRelatedProvider implementation="fr.adrienbrault.idea.symfony2plugin.navigation.PhpGotoRelatedProvider"/>
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.codeInsight;
2+
3+
import com.intellij.psi.PsiFile;
4+
import com.jetbrains.php.lang.PhpFileType;
5+
import com.jetbrains.php.lang.psi.PhpFile;
6+
import com.jetbrains.php.lang.psi.elements.Method;
7+
import com.jetbrains.php.lang.psi.elements.PhpClass;
8+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.SymfonyImplicitUsageProvider;
9+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
10+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
11+
import org.apache.commons.lang.StringUtils;
12+
import org.jetbrains.annotations.NotNull;
13+
14+
import java.util.Arrays;
15+
16+
/**
17+
* @author Daniel Espendiller <daniel@espendiller.net>
18+
* @see SymfonyImplicitUsageProvider
19+
*/
20+
public class SymfonyImplicitUsageProviderTest extends SymfonyLightCodeInsightFixtureTestCase {
21+
public void setUp() throws Exception {
22+
super.setUp();
23+
24+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("routes.yml"));
25+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("classes.php"));
26+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("services.yml"));
27+
}
28+
29+
public String getTestDataPath() {
30+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/codeInsight/fixtures";
31+
}
32+
33+
public void testControllerClassIsUsedWhenAMethodHasRoute() {
34+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent("" +
35+
"#[Route()]\n" +
36+
"public function foo2() {}"
37+
)));
38+
39+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent("" +
40+
"/**\n" +
41+
"* @Route()\n" +
42+
"*/\n" +
43+
"public function foo() {}\n"
44+
)));
45+
}
46+
47+
public void testControllerClassIsUnusedIfRoutesArePrivate() {
48+
assertFalse(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent("" +
49+
"/**\n" +
50+
"* @Route()\n" +
51+
"*/\n" +
52+
"private function foo() {}\n" +
53+
"#[Route()]\n" +
54+
"private function foo2() {}"
55+
)));
56+
}
57+
58+
public void testControllerMethodIsUsedWhenAMethodIsHasRouteDefinition() {
59+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerMethodWithRouteContent("" +
60+
"/**\n" +
61+
"* @Route()\n" +
62+
"*/\n" +
63+
"public function foobar() {}"
64+
)));
65+
66+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerMethodWithRouteContent("" +
67+
"#[Route()]\n" +
68+
"public function foobar() {}"
69+
)));
70+
}
71+
72+
public void testControllerMethodIsUntouchedForPrivateMethods() {
73+
assertFalse(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerMethodWithRouteContent("" +
74+
"/**\n" +
75+
"* @Route()\n" +
76+
"*/\n" +
77+
"private function foobar() {}"
78+
)));
79+
80+
assertFalse(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerMethodWithRouteContent("" +
81+
"#[Route()]\n" +
82+
"private function foobar() {}"
83+
)));
84+
}
85+
86+
public void testControllerForDefinitionInsideYaml() {
87+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent("" +
88+
"public function foobarYaml() {}"
89+
)));
90+
91+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent(
92+
"\\App\\Controller\\FooControllerInvoke",
93+
"public function __invoke() {}"
94+
)));
95+
}
96+
97+
public void testControllerForDefinitionInsideYamlWithAction() {
98+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent("" +
99+
"public function foobarYamlAction() {}"
100+
)));
101+
}
102+
103+
public void testControllerForDefinitionInsideYamlAsService() {
104+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent(
105+
"\\App\\Controller\\FooControllerService",
106+
"public function foo() {}"
107+
)));
108+
109+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent(
110+
"\\App\\Controller\\FooControllerService",
111+
"public function foo() {}"
112+
)));
113+
114+
assertTrue(new SymfonyImplicitUsageProvider().isImplicitUsage(createPhpControllerClassWithRouteContent(
115+
"\\App\\Controller\\FooControllerServiceInvoke",
116+
"public function __invoke() {}"
117+
)));
118+
}
119+
120+
private PhpClass createPhpControllerClassWithRouteContent(@NotNull String content) {
121+
return createPhpControllerClassWithRouteContent("\\App\\Controller\\FooController", content);
122+
}
123+
124+
private PhpClass createPhpControllerClassWithRouteContent(@NotNull String className, @NotNull String content) {
125+
String[] split = StringUtils.stripStart(className, "\\").split("\\\\");
126+
127+
PsiFile psiFile = myFixture.configureByText(PhpFileType.INSTANCE, "<?php" +
128+
"<?php\n" +
129+
"namespace " + StringUtils.join(Arrays.copyOf(split, split.length - 1), "\\") + ";\n" +
130+
"\n" +
131+
"use Symfony\\Component\\Routing\\Annotation\\Route;\n" +
132+
"\n" +
133+
"class " + split[split.length - 1] + "\n" +
134+
"{\n" +
135+
"" + content + "\n" +
136+
"}"
137+
);
138+
139+
return PhpElementsUtil.getFirstClassFromFile((PhpFile) psiFile.getContainingFile());
140+
}
141+
142+
@NotNull
143+
private Method createPhpControllerMethodWithRouteContent(@NotNull String content) {
144+
PhpClass phpClass = createPhpControllerClassWithRouteContent("\\App\\Controller\\FooController", content);
145+
return phpClass.getMethods().iterator().next();
146+
}
147+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Symfony\Component\Routing\Annotation
4+
{
5+
/**
6+
* @Annotation
7+
*/
8+
class Route {}
9+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
foo_controller:
2+
pattern: /
3+
defaults:
4+
_controller: App\Controller\FooController::foobarYaml
5+
6+
foo_controller_invoke:
7+
pattern: /
8+
defaults:
9+
_controller: App\Controller\FooControllerInvoke
10+
11+
foo_controller_service_route:
12+
pattern: /
13+
defaults:
14+
_controller: foo_controller_service:foo
15+
16+
foo_controller_service_route_invoke:
17+
pattern: /
18+
defaults:
19+
_controller: foo_controller.service_invoke
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
foo_controller_service:
3+
class: App\Controller\FooControllerService
4+
5+
foo_controller.service_invoke:
6+
class: App\Controller\FooControllerServiceInvoke

0 commit comments

Comments
 (0)