From c8a4c7f45920ac63ef59f2ebb0a3a116b002ff52 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Mon, 20 May 2024 13:00:01 +0200 Subject: [PATCH] #2348 provde FQCN-based Routes support (only compiled provider) / [Twig] Escaping route names with backslashes --- .../symfony2plugin/routing/RouteHelper.java | 69 +++++++++++++++++-- .../routing/RouteLookupElement.java | 9 +++ .../inspection/PhpRouteMissingInspection.java | 6 +- .../TwigRouteMissingInspection.java | 6 +- .../TwigTemplateCompletionContributor.java | 2 +- .../TwigTemplateGoToDeclarationHandler.java | 6 +- .../TwigHtmlCompletionContributor.java | 2 +- .../TwigRouteMissingInspectionTest.java | 13 ++++ .../fixtures/TwigRouteMissingInspection.php | 14 ++++ ...wigTemplateGoToDeclarationHandlerTest.java | 14 ++++ ...wigTemplateGoToLocalDeclarationHandler.php | 14 ++++ 11 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/fixtures/TwigRouteMissingInspection.php diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java index 4d3394497..9a33ce9a7 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteHelper.java @@ -115,6 +115,22 @@ public static Collection getRoute(@NotNull Project project, @NotNull Stri return routes; } + public static boolean isExistingRouteName(@NotNull Project project, @NotNull String routeName) { + return getFQCNRoute(project, routeName) != null + || !getRoute(project, routeName).isEmpty(); + } + + /** + * "App\\Controller\\Car\\ZipController::index" => "App\Controller\Car\ZipController::index" + */ + public static String unescapeRouteName(@NotNull String routeName) { + if (routeName.contains("\\")) { + return routeName.replace("\\\\", "\\"); + } + + return routeName; + } + public static PsiElement[] getRouteParameterPsiElements(@NotNull Project project, @NotNull String routeName, @NotNull String parameterName) { Collection results = new ArrayList<>(); @@ -134,6 +150,11 @@ public static PsiElement[] getRouteParameterPsiElements(@NotNull Project project public static PsiElement[] getMethods(@NotNull Project project, @NotNull String routeName) { Set targets = new HashSet<>(); + Method fqcnRoute = getFQCNRoute(project, routeName); + if (fqcnRoute != null) { + targets.add(fqcnRoute); + } + for (Route route : getRoute(project, routeName)) { targets.addAll(Arrays.asList(getMethodsOnControllerShortcut(project, route.getController()))); } @@ -141,6 +162,34 @@ public static PsiElement[] getMethods(@NotNull Project project, @NotNull String return targets.toArray(new PsiElement[0]); } + /** + * "App\Controller\Foo" + * "App\Controller\Foo::method" + */ + public static Method getFQCNRoute(@NotNull Project project, @NotNull String routeName) { + if (!routeName.contains("\\")) { + return null; + } + + String className = null; + String method = null; + + if (routeName.contains(":")) { + String[] split = routeName.split("::"); + if (split.length == 2) { + className = "\\" + StringUtils.stripStart(split[0], "\\"); + method = split[1]; + } + } else { + className = "\\" + StringUtils.stripStart(routeName, "\\"); + method = "__invoke"; + } + + return className != null && method != null + ? PhpElementsUtil.getClassMethod(project, className, method) + : null; + } + public static Collection findRoutesByPath(@NotNull Project project, @NotNull String path) { return RouteHelper.getAllRoutes(project) .values() @@ -1285,15 +1334,19 @@ public static String getRouteUrl(Route route) { return url.isEmpty() ? null : url; } - public static List getRoutesLookupElements(final @NotNull Project project) { - + public static List getRoutesLookupElements(final @NotNull Project project, boolean escapeRouteName) { Map routes = RouteHelper.getCompiledRoutes(project); final List lookupElements = new ArrayList<>(); final Set uniqueSet = new HashSet<>(); for (Route route : routes.values()) { - lookupElements.add(new RouteLookupElement(route)); + RouteLookupElement lookupElement = new RouteLookupElement(route); + if (escapeRouteName) { + lookupElement.withEscape(); + } + + lookupElements.add(lookupElement); uniqueSet.add(route.getName()); } @@ -1303,13 +1356,21 @@ public static List getRoutesLookupElements(final @NotNull Project } for (StubIndexedRoute route: FileBasedIndex.getInstance().getValues(RoutesStubIndex.KEY, routeName, GlobalSearchScope.allScope(project))) { - lookupElements.add(new RouteLookupElement(new Route(route), true)); + RouteLookupElement lookupElement = new RouteLookupElement(new Route(route), true); + if (escapeRouteName) { + lookupElement.withEscape(); + } + + lookupElements.add(lookupElement); uniqueSet.add(routeName); } } return lookupElements; + } + public static List getRoutesLookupElements(final @NotNull Project project) { + return getRoutesLookupElements(project, false); } @NotNull diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteLookupElement.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteLookupElement.java index ed88ec216..782f33fed 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteLookupElement.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/RouteLookupElement.java @@ -23,6 +23,7 @@ public class RouteLookupElement extends LookupElement { final private Route route; private boolean isWeak = false; private InsertHandler insertHandler; + private boolean escape = false; public RouteLookupElement(@NotNull Route route) { this.route = route; @@ -36,6 +37,10 @@ public RouteLookupElement(@NotNull Route route, boolean isWeak) { @NotNull @Override public String getLookupString() { + if (escape) { + return route.getName().replace("\\", "\\\\"); + } + return route.getName(); } @@ -81,6 +86,10 @@ public void withInsertHandler(InsertHandler insertHandler) { this.insertHandler = insertHandler; } + public void withEscape() { + this.escape = true; + } + public Route getRoute() { return route; } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/PhpRouteMissingInspection.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/PhpRouteMissingInspection.java index a27559107..f2cf7313e 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/PhpRouteMissingInspection.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/PhpRouteMissingInspection.java @@ -7,15 +7,12 @@ import com.intellij.psi.PsiElementVisitor; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.routing.PhpRouteReferenceContributor; -import fr.adrienbrault.idea.symfony2plugin.routing.Route; import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper; import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; -import java.util.Collection; - /** * @author Daniel Espendiller */ @@ -51,8 +48,7 @@ private void invoke(@NotNull String routeName, @NotNull final PsiElement element return; } - Collection route = RouteHelper.getRoute(element.getProject(), routeName); - if(route.isEmpty()) { + if (!RouteHelper.isExistingRouteName(element.getProject(), routeName)) { holder.registerProblem(element, "Symfony: Missing Route", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, new RouteGuessTypoQuickFix(routeName)); } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/TwigRouteMissingInspection.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/TwigRouteMissingInspection.java index 8e1362c1e..188163647 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/TwigRouteMissingInspection.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/TwigRouteMissingInspection.java @@ -23,7 +23,7 @@ public PsiElementVisitor buildVisitor(final @NotNull ProblemsHolder holder, bool return new PsiElementVisitor() { @Override - public void visitElement(PsiElement element) { + public void visitElement(@NotNull PsiElement element) { if(TwigPattern.getAutocompletableRoutePattern().accepts(element) && TwigUtil.isValidStringWithoutInterpolatedOrConcat(element)) { invoke(element, holder); } @@ -35,11 +35,11 @@ public void visitElement(PsiElement element) { private void invoke(@NotNull final PsiElement element, @NotNull ProblemsHolder holder) { String text = element.getText(); - if(StringUtils.isBlank(text)) { + if (StringUtils.isBlank(text)) { return; } - if(RouteHelper.getRoute(element.getProject(), text).isEmpty()) { + if (!RouteHelper.isExistingRouteName(element.getProject(), RouteHelper.unescapeRouteName(text))) { holder.registerProblem(element, "Symfony: Missing Route", new RouteGuessTypoQuickFix(text)); } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java index c213c954d..4bcca0527 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java @@ -345,7 +345,7 @@ public void addCompletions(@NotNull CompletionParameters parameters, @NotNull Pr return; } - resultSet.addAllElements(RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject())); + resultSet.addAllElements(RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject(), true)); } } ); diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java index 285cd0660..ea0a947b7 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java @@ -300,12 +300,12 @@ public static Collection getFilterGoTo(@NotNull PsiElement psiEleme private Collection getRouteGoTo(@NotNull PsiElement psiElement) { String text = PsiElementUtils.getText(psiElement); - if(StringUtils.isBlank(text)) { + if (StringUtils.isBlank(text)) { return Collections.emptyList(); } - PsiElement[] methods = RouteHelper.getMethods(psiElement.getProject(), text); - if(methods.length > 0) { + PsiElement[] methods = RouteHelper.getMethods(psiElement.getProject(), RouteHelper.unescapeRouteName(text)); + if (methods.length > 0) { return Arrays.asList(methods); } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/completion/TwigHtmlCompletionContributor.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/completion/TwigHtmlCompletionContributor.java index c2d7fcc6e..9d46779de 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/completion/TwigHtmlCompletionContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/completion/TwigHtmlCompletionContributor.java @@ -66,7 +66,7 @@ protected void addCompletions(@NotNull CompletionParameters parameters, Processi return; } - List routesLookupElements = RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject()); + List routesLookupElements = RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject(), true); for (LookupElement element : routesLookupElements) { if (element instanceof RouteLookupElement) { ((RouteLookupElement) element).withInsertHandler(TwigPathFunctionInsertHandler.getInstance()); diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/TwigRouteMissingInspectionTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/TwigRouteMissingInspectionTest.java index ae5329e14..ac57d9d76 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/TwigRouteMissingInspectionTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/TwigRouteMissingInspectionTest.java @@ -11,6 +11,7 @@ public void setUp() throws Exception { super.setUp(); myFixture.copyFileToProject("TwigRouteMissingInspection.xml"); + myFixture.copyFileToProject("TwigRouteMissingInspection.php"); } protected String getTestDataPath() { @@ -31,6 +32,18 @@ public void testThatKnownRouteMustNotProvideErrorHighlight() { "{{ path('my_foobar') }}", "Symfony: Missing Route" ); + + assertLocalInspectionNotContains( + "test.html.twig", + "{{ path('App\\\\Controller\\\\FoobarController') }}", + "Symfony: Missing Route" + ); + + assertLocalInspectionNotContains( + "test.html.twig", + "{{ path('App\\\\Controller\\\\FooController::foobar') }}", + "Symfony: Missing Route" + ); } public void testThatInterpolatedStringMustBeIgnoredForInspection() { diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/fixtures/TwigRouteMissingInspection.php b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/fixtures/TwigRouteMissingInspection.php new file mode 100644 index 000000000..fb5261e91 --- /dev/null +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/fixtures/TwigRouteMissingInspection.php @@ -0,0 +1,14 @@ +Controller') }}", + PlatformPatterns.psiElement(Method.class) + ); + + assertNavigationMatch( + TwigFileType.INSTANCE, + "{{ path('App\\\\Controller\\\\FooController::foobar') }}", + PlatformPatterns.psiElement(Method.class) + ); + } } diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php index 33cfa6706..1fcb7874b 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php @@ -61,3 +61,17 @@ enum FooEnum case BAR1; } } + + +namespace App\Controller +{ + class FooController + { + public function foobar() {} + } + + class FoobarController + { + public function __invoke() {} + } +} \ No newline at end of file