Skip to content

Commit b8cbd6c

Browse files
authored
Merge pull request #2374 from Haehnchen/feature/2348-route-escape
#2348 provde FQCN-based Routes support (only compiled provider) / [Twig] Escaping route names with backslashes
2 parents 530529f + c8a4c7f commit b8cbd6c

11 files changed

+138
-17
lines changed

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

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@ public static Collection<Route> getRoute(@NotNull Project project, @NotNull Stri
115115
return routes;
116116
}
117117

118+
public static boolean isExistingRouteName(@NotNull Project project, @NotNull String routeName) {
119+
return getFQCNRoute(project, routeName) != null
120+
|| !getRoute(project, routeName).isEmpty();
121+
}
122+
123+
/**
124+
* "App\\Controller\\Car\\ZipController::index" => "App\Controller\Car\ZipController::index"
125+
*/
126+
public static String unescapeRouteName(@NotNull String routeName) {
127+
if (routeName.contains("\\")) {
128+
return routeName.replace("\\\\", "\\");
129+
}
130+
131+
return routeName;
132+
}
133+
118134
public static PsiElement[] getRouteParameterPsiElements(@NotNull Project project, @NotNull String routeName, @NotNull String parameterName) {
119135
Collection<PsiElement> results = new ArrayList<>();
120136

@@ -134,13 +150,46 @@ public static PsiElement[] getRouteParameterPsiElements(@NotNull Project project
134150
public static PsiElement[] getMethods(@NotNull Project project, @NotNull String routeName) {
135151
Set<PsiElement> targets = new HashSet<>();
136152

153+
Method fqcnRoute = getFQCNRoute(project, routeName);
154+
if (fqcnRoute != null) {
155+
targets.add(fqcnRoute);
156+
}
157+
137158
for (Route route : getRoute(project, routeName)) {
138159
targets.addAll(Arrays.asList(getMethodsOnControllerShortcut(project, route.getController())));
139160
}
140161

141162
return targets.toArray(new PsiElement[0]);
142163
}
143164

165+
/**
166+
* "App\Controller\Foo"
167+
* "App\Controller\Foo::method"
168+
*/
169+
public static Method getFQCNRoute(@NotNull Project project, @NotNull String routeName) {
170+
if (!routeName.contains("\\")) {
171+
return null;
172+
}
173+
174+
String className = null;
175+
String method = null;
176+
177+
if (routeName.contains(":")) {
178+
String[] split = routeName.split("::");
179+
if (split.length == 2) {
180+
className = "\\" + StringUtils.stripStart(split[0], "\\");
181+
method = split[1];
182+
}
183+
} else {
184+
className = "\\" + StringUtils.stripStart(routeName, "\\");
185+
method = "__invoke";
186+
}
187+
188+
return className != null && method != null
189+
? PhpElementsUtil.getClassMethod(project, className, method)
190+
: null;
191+
}
192+
144193
public static Collection<Route> findRoutesByPath(@NotNull Project project, @NotNull String path) {
145194
return RouteHelper.getAllRoutes(project)
146195
.values()
@@ -1285,15 +1334,19 @@ public static String getRouteUrl(Route route) {
12851334
return url.isEmpty() ? null : url;
12861335
}
12871336

1288-
public static List<LookupElement> getRoutesLookupElements(final @NotNull Project project) {
1289-
1337+
public static List<LookupElement> getRoutesLookupElements(final @NotNull Project project, boolean escapeRouteName) {
12901338
Map<String, Route> routes = RouteHelper.getCompiledRoutes(project);
12911339

12921340
final List<LookupElement> lookupElements = new ArrayList<>();
12931341

12941342
final Set<String> uniqueSet = new HashSet<>();
12951343
for (Route route : routes.values()) {
1296-
lookupElements.add(new RouteLookupElement(route));
1344+
RouteLookupElement lookupElement = new RouteLookupElement(route);
1345+
if (escapeRouteName) {
1346+
lookupElement.withEscape();
1347+
}
1348+
1349+
lookupElements.add(lookupElement);
12971350
uniqueSet.add(route.getName());
12981351
}
12991352

@@ -1303,13 +1356,21 @@ public static List<LookupElement> getRoutesLookupElements(final @NotNull Project
13031356
}
13041357

13051358
for (StubIndexedRoute route: FileBasedIndex.getInstance().getValues(RoutesStubIndex.KEY, routeName, GlobalSearchScope.allScope(project))) {
1306-
lookupElements.add(new RouteLookupElement(new Route(route), true));
1359+
RouteLookupElement lookupElement = new RouteLookupElement(new Route(route), true);
1360+
if (escapeRouteName) {
1361+
lookupElement.withEscape();
1362+
}
1363+
1364+
lookupElements.add(lookupElement);
13071365
uniqueSet.add(routeName);
13081366
}
13091367
}
13101368

13111369
return lookupElements;
1370+
}
13121371

1372+
public static List<LookupElement> getRoutesLookupElements(final @NotNull Project project) {
1373+
return getRoutesLookupElements(project, false);
13131374
}
13141375

13151376
@NotNull

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class RouteLookupElement extends LookupElement {
2323
final private Route route;
2424
private boolean isWeak = false;
2525
private InsertHandler<RouteLookupElement> insertHandler;
26+
private boolean escape = false;
2627

2728
public RouteLookupElement(@NotNull Route route) {
2829
this.route = route;
@@ -36,6 +37,10 @@ public RouteLookupElement(@NotNull Route route, boolean isWeak) {
3637
@NotNull
3738
@Override
3839
public String getLookupString() {
40+
if (escape) {
41+
return route.getName().replace("\\", "\\\\");
42+
}
43+
3944
return route.getName();
4045
}
4146

@@ -81,6 +86,10 @@ public void withInsertHandler(InsertHandler<RouteLookupElement> insertHandler) {
8186
this.insertHandler = insertHandler;
8287
}
8388

89+
public void withEscape() {
90+
this.escape = true;
91+
}
92+
8493
public Route getRoute() {
8594
return route;
8695
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/PhpRouteMissingInspection.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@
77
import com.intellij.psi.PsiElementVisitor;
88
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
99
import fr.adrienbrault.idea.symfony2plugin.routing.PhpRouteReferenceContributor;
10-
import fr.adrienbrault.idea.symfony2plugin.routing.Route;
1110
import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper;
1211
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
1312
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
1413
import org.apache.commons.lang3.StringUtils;
1514
import org.jetbrains.annotations.NotNull;
1615

17-
import java.util.Collection;
18-
1916
/**
2017
* @author Daniel Espendiller <daniel@espendiller.net>
2118
*/
@@ -51,8 +48,7 @@ private void invoke(@NotNull String routeName, @NotNull final PsiElement element
5148
return;
5249
}
5350

54-
Collection<Route> route = RouteHelper.getRoute(element.getProject(), routeName);
55-
if(route.isEmpty()) {
51+
if (!RouteHelper.isExistingRouteName(element.getProject(), routeName)) {
5652
holder.registerProblem(element, "Symfony: Missing Route", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, new RouteGuessTypoQuickFix(routeName));
5753
}
5854
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/routing/inspection/TwigRouteMissingInspection.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public PsiElementVisitor buildVisitor(final @NotNull ProblemsHolder holder, bool
2323

2424
return new PsiElementVisitor() {
2525
@Override
26-
public void visitElement(PsiElement element) {
26+
public void visitElement(@NotNull PsiElement element) {
2727
if(TwigPattern.getAutocompletableRoutePattern().accepts(element) && TwigUtil.isValidStringWithoutInterpolatedOrConcat(element)) {
2828
invoke(element, holder);
2929
}
@@ -35,11 +35,11 @@ public void visitElement(PsiElement element) {
3535

3636
private void invoke(@NotNull final PsiElement element, @NotNull ProblemsHolder holder) {
3737
String text = element.getText();
38-
if(StringUtils.isBlank(text)) {
38+
if (StringUtils.isBlank(text)) {
3939
return;
4040
}
4141

42-
if(RouteHelper.getRoute(element.getProject(), text).isEmpty()) {
42+
if (!RouteHelper.isExistingRouteName(element.getProject(), RouteHelper.unescapeRouteName(text))) {
4343
holder.registerProblem(element, "Symfony: Missing Route", new RouteGuessTypoQuickFix(text));
4444
}
4545
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ public void addCompletions(@NotNull CompletionParameters parameters, @NotNull Pr
345345
return;
346346
}
347347

348-
resultSet.addAllElements(RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject()));
348+
resultSet.addAllElements(RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject(), true));
349349
}
350350
}
351351
);

src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,12 +300,12 @@ public static Collection<PsiElement> getFilterGoTo(@NotNull PsiElement psiEleme
300300
private Collection<PsiElement> getRouteGoTo(@NotNull PsiElement psiElement) {
301301
String text = PsiElementUtils.getText(psiElement);
302302

303-
if(StringUtils.isBlank(text)) {
303+
if (StringUtils.isBlank(text)) {
304304
return Collections.emptyList();
305305
}
306306

307-
PsiElement[] methods = RouteHelper.getMethods(psiElement.getProject(), text);
308-
if(methods.length > 0) {
307+
PsiElement[] methods = RouteHelper.getMethods(psiElement.getProject(), RouteHelper.unescapeRouteName(text));
308+
if (methods.length > 0) {
309309
return Arrays.asList(methods);
310310
}
311311

src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/completion/TwigHtmlCompletionContributor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ protected void addCompletions(@NotNull CompletionParameters parameters, Processi
6666
return;
6767
}
6868

69-
List<LookupElement> routesLookupElements = RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject());
69+
List<LookupElement> routesLookupElements = RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject(), true);
7070
for (LookupElement element : routesLookupElements) {
7171
if (element instanceof RouteLookupElement) {
7272
((RouteLookupElement) element).withInsertHandler(TwigPathFunctionInsertHandler.getInstance());

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/routing/inspection/TwigRouteMissingInspectionTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public void setUp() throws Exception {
1111
super.setUp();
1212

1313
myFixture.copyFileToProject("TwigRouteMissingInspection.xml");
14+
myFixture.copyFileToProject("TwigRouteMissingInspection.php");
1415
}
1516

1617
protected String getTestDataPath() {
@@ -31,6 +32,18 @@ public void testThatKnownRouteMustNotProvideErrorHighlight() {
3132
"{{ path('my_<caret>foobar') }}",
3233
"Symfony: Missing Route"
3334
);
35+
36+
assertLocalInspectionNotContains(
37+
"test.html.twig",
38+
"{{ path('App\\\\Controller\\\\Foobar<caret>Controller') }}",
39+
"Symfony: Missing Route"
40+
);
41+
42+
assertLocalInspectionNotContains(
43+
"test.html.twig",
44+
"{{ path('App\\\\Controller\\\\FooCon<caret>troller::foobar') }}",
45+
"Symfony: Missing Route"
46+
);
3447
}
3548

3649
public void testThatInterpolatedStringMustBeIgnoredForInspection() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace App\Controller
4+
{
5+
class FooController
6+
{
7+
public function foobar() {}
8+
}
9+
10+
class FoobarController
11+
{
12+
public function __invoke() {}
13+
}
14+
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,18 @@ public void testComponentNameTagNavigation() {
300300
PlatformPatterns.psiElement(PhpClass.class)
301301
);
302302
}
303+
304+
public void testFoo() {
305+
assertNavigationMatch(
306+
TwigFileType.INSTANCE,
307+
"{{ path('App\\\\Controller\\\\Foobar<caret>Controller') }}",
308+
PlatformPatterns.psiElement(Method.class)
309+
);
310+
311+
assertNavigationMatch(
312+
TwigFileType.INSTANCE,
313+
"{{ path('App\\\\Controller\\\\FooCo<caret>ntroller::foobar') }}",
314+
PlatformPatterns.psiElement(Method.class)
315+
);
316+
}
303317
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,17 @@ enum FooEnum
6161
case BAR1;
6262
}
6363
}
64+
65+
66+
namespace App\Controller
67+
{
68+
class FooController
69+
{
70+
public function foobar() {}
71+
}
72+
73+
class FoobarController
74+
{
75+
public function __invoke() {}
76+
}
77+
}

0 commit comments

Comments
 (0)