package de.espend.idea.laravel.routing.utils; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.*; import com.intellij.util.indexing.FileBasedIndex; import com.jetbrains.php.lang.PhpFileType; import com.jetbrains.php.lang.psi.elements.*; import de.espend.idea.laravel.stub.RouteIndexExtension; import de.espend.idea.laravel.stub.processor.CollectProjectUniqueKeys; import fr.adrienbrault.idea.symfony2plugin.codeInsight.utils.PhpElementsUtil; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import java.util.*; /** * @author Daniel Espendiller <[email protected]> */ public class RoutingUtil { static final String[] HTTP_METHODS = new String[]{"get", "post", "put", "delete", "patch", "delete", "options", "any", "match"}; private static final Key<CachedValue<Collection<String>>> ROUTE_NAMES = new Key<>("LaravelRoutingUtilNames"); static final String[] REST_METHODS = new String[]{"index", "create", "store", "show", "edit", "update", "destroy"}; public static Collection<PsiElement> getRoutesAsTargets(@NotNull PsiFile psiFile, final @NotNull String routeName) { final Set<PsiElement> names = new HashSet<>(); visitRoutesForAs(psiFile, (psiElement, name) -> { if(name.equals(routeName)) { names.add(psiElement); } }); return names; } public static Collection<PsiElement> getRoutesAsTargets(@NotNull Project project, @NotNull String routeName) { Set<PsiElement> targets = new HashSet<>(); Set<VirtualFile> virtualFiles = new HashSet<>(); // find files with route name FileBasedIndex.getInstance().getFilesWithKey(RouteIndexExtension.KEY, Collections.singleton(routeName), virtualFile -> { virtualFiles.add(virtualFile); return true; }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), PhpFileType.INSTANCE)); // resolve virtual files and collect for (VirtualFile virtualFile : virtualFiles) { PsiFile file = PsiManager.getInstance(project).findFile(virtualFile); if(file != null) { targets.addAll(getRoutesAsTargets(file, routeName)); } } return targets; } @NotNull public static Collection<String> getRoutesAsNames(final @NotNull Project project) { CachedValue<Collection<String>> cache = project.getUserData(ROUTE_NAMES); if(cache == null) { cache = CachedValuesManager.getManager(project).createCachedValue(() -> { Collection<String> names = new HashSet<>( CollectProjectUniqueKeys.collect(project, RouteIndexExtension.KEY) ); return CachedValueProvider.Result.create(names, PsiModificationTracker.MODIFICATION_COUNT); }, false); project.putUserData(ROUTE_NAMES, cache); } return cache.getValue(); } public static Collection<String> getRoutesAsNames(@NotNull PsiFile psiFile) { final Set<String> names = new HashSet<>(); visitRoutesForAs(psiFile, (psiElement, name) -> names.add(name) ); return names; } public static void visitRoutesForAs(@NotNull PsiFile psiFile, @NotNull RouteAsNameVisitor visitor) { psiFile.acceptChildren(new RouteNamePsiRecursiveElementVisitor(visitor)); } public interface RouteAsNameVisitor { void visit(@NotNull PsiElement psiElement, @NotNull String name); } private static class RouteNamePsiRecursiveElementVisitor extends PsiRecursiveElementVisitor { @NotNull private final RouteAsNameVisitor visitor; public RouteNamePsiRecursiveElementVisitor(@NotNull RouteAsNameVisitor visitor) { this.visitor = visitor; } @Override public void visitElement(PsiElement element) { if(element instanceof MethodReference) { if("name".equals(((MethodReference) element).getName())) { // Route::get('foo')->name('foo') for (MethodReference methodReference : PsiTreeUtil.getChildrenOfTypeAsList(element, MethodReference.class)) { PhpPsiElement classReference = methodReference.getFirstPsiChild(); if(classReference instanceof ClassReference) { if("Route".equalsIgnoreCase(classReference.getName())) { visitName((MethodReference) element, this.getRouteNamePrefix(element)); return; } } } } else if(ArrayUtils.contains(HTTP_METHODS, ((MethodReference) element).getName())) { // Route::get('foo', ['as' => ...]) PhpPsiElement classReference = ((MethodReference) element).getFirstPsiChild(); if(classReference instanceof ClassReference) { if("Route".equalsIgnoreCase(classReference.getName())) { visitAs((MethodReference) element, this.getRouteNamePrefix(element)); } } } else if("resource".equals(((MethodReference) element).getName())) { // Route::resource('foo', 'FooController', [...]) PhpPsiElement classReference = ((MethodReference) element).getFirstPsiChild(); if(classReference instanceof ClassReference) { if("Route".equalsIgnoreCase(classReference.getName())) { visitResource((MethodReference) element, this.getRouteNamePrefix(element)); } } } } super.visitElement(element); } /** * Returns route name prefix, based on Route::group(['as' => values */ @NotNull private String getRouteNamePrefix(PsiElement element) { return StringUtils.join(RouteGroupUtil.getRouteGroupPropertiesCollection(element, "as"), ""); } private void visitAs(@NotNull MethodReference methodReference, @NotNull String prefix) { PsiElement[] parameters = methodReference.getParameters(); int indexParameter = 1; if("match".equals(methodReference.getName())){ indexParameter = 2; } if(parameters.length < (1+indexParameter) || !(parameters[indexParameter] instanceof ArrayCreationExpression)) { return; } PhpPsiElement arrayValue = PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameters[indexParameter], "as"); if(!(arrayValue instanceof StringLiteralExpression)) { return; } String contents = ((StringLiteralExpression) arrayValue).getContents(); if(StringUtils.isBlank(contents)) { return; } this.visitor.visit(arrayValue, prefix + contents); } private void visitName(@NotNull MethodReference methodReference, @NotNull String prefix) { PsiElement[] parameters = methodReference.getParameters(); if(parameters.length < 1 || !(parameters[0] instanceof StringLiteralExpression)) { return; } String contents = ((StringLiteralExpression) parameters[0]).getContents(); if(StringUtils.isBlank(contents)) { return; } this.visitor.visit(parameters[0], prefix + contents); } /** * Visiting Route::resource('foo', 'FooController', [...]) * * @param methodReference Route::resource element * @param prefix Prefix got from ['as' => ...] values from parent Route::group elements */ private void visitResource(@NotNull MethodReference methodReference, @NotNull String prefix) { PsiElement[] parameters = methodReference.getParameters(); if(parameters.length < 2 || !(parameters[0] instanceof StringLiteralExpression)) { return; } String routeUrl = ((StringLiteralExpression) parameters[0]).getContents(); if(StringUtils.isBlank(routeUrl)) { return; } String[] routeUrlParts = routeUrl.replace('/', '.').split("\\."); if(routeUrlParts.length == 0) { return; } String baseNamesPrefix = routeUrlParts[routeUrlParts.length - 1]; // Only last part of the url goes to route name for(String routeName : getResourceRouteNames(parameters, prefix + baseNamesPrefix + ".")) { this.visitor.visit(parameters[0], routeName); } } /** * @param parameters Route::resource method reference parameters * @return Collection of full Route::resource names, like ["users.index", "users.show"] */ private Collection<String> getResourceRouteNames(@NotNull PsiElement[] parameters, @NotNull String prefix) { Map<String, String> restMethods = new HashMap<>(); for(String method : REST_METHODS) { restMethods.put(method, prefix + method); } if(parameters.length < 3 || !(parameters[2] instanceof ArrayCreationExpression)) { return restMethods.values(); } // Route::resource(..., ['only' => []]) PhpPsiElement onlyValue = PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameters[2], "only"); Map<String, String> resultMethods; if(onlyValue instanceof ArrayCreationExpression) { resultMethods = new HashMap<>(); for(String method : PhpElementsUtil.getArrayValuesAsString(((ArrayCreationExpression) onlyValue))) { if(restMethods.containsKey(method)) { resultMethods.put(method, prefix + method); } } } else { // Route::resource(..., ['except' => []]) PhpPsiElement exceptValue = PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameters[2], "except"); if(exceptValue instanceof ArrayCreationExpression) { resultMethods = restMethods; for(String method : PhpElementsUtil.getArrayValuesAsString(((ArrayCreationExpression) exceptValue))) { resultMethods.remove(method); } } else { resultMethods = restMethods; } } // Route::resource(..., ['values' => []]) it overrides standard route names PhpPsiElement namesValue = PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameters[2], "names"); if(namesValue instanceof ArrayCreationExpression) { resultMethods = restMethods; for(Map.Entry<String, PsiElement> entry : PhpElementsUtil.getArrayValueMap(((ArrayCreationExpression) namesValue)).entrySet()) { if(entry.getValue() instanceof StringLiteralExpression && resultMethods.containsKey(entry.getKey())) { resultMethods.replace(entry.getKey(), ((StringLiteralExpression) entry.getValue()).getContents()); } } } return resultMethods.values(); } } }