package net.bitpot.railways.utils; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiNamedElement; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.stubs.StubIndex; import com.intellij.psi.util.PsiElementFilter; import com.intellij.psi.util.PsiTreeUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.ruby.rails.model.RailsApp; import org.jetbrains.plugins.ruby.rails.model.RailsController; import org.jetbrains.plugins.ruby.ruby.lang.psi.RPsiElement; import org.jetbrains.plugins.ruby.ruby.lang.psi.RubyProjectAndLibrariesScope; import org.jetbrains.plugins.ruby.ruby.lang.psi.RubyPsiUtil; import org.jetbrains.plugins.ruby.ruby.lang.psi.controlStructures.classes.RClass; import org.jetbrains.plugins.ruby.ruby.lang.psi.controlStructures.methods.RMethod; import org.jetbrains.plugins.ruby.ruby.lang.psi.controlStructures.modules.RModule; import org.jetbrains.plugins.ruby.ruby.lang.psi.controlStructures.names.RSuperClass; import org.jetbrains.plugins.ruby.ruby.lang.psi.holders.RContainer; import org.jetbrains.plugins.ruby.ruby.lang.psi.indexes.RubyClassModuleNameIndex; import org.jetbrains.plugins.ruby.ruby.lang.psi.methodCall.RCall; import org.jetbrains.plugins.ruby.utils.NamingConventions; import java.util.Collection; /** * Class that contains helper methods for working with PSI elements. * * Created by Basil Gren on 11/27/14. */ public class RailwaysPsiUtils { /** * Searches for controller in application and libraries. * * @param app Rails app for the current module * @param qualifiedClassName Full class name with modules, ex. "Devise::SessionsController" * @return Found RClass object or null if nothing is found. */ @Nullable public static RClass findControllerClass(RailsApp app, String qualifiedClassName) { if ((app == null) || qualifiedClassName.isEmpty()) return null; // Lookup in application controllers RailsController ctrl = app.findController(qualifiedClassName); if (ctrl != null) return ctrl.getRClass(); // If controller is not found among application classes, proceed with // global class lookup RContainer cont = findClassOrModule(qualifiedClassName, app.getProject()); return cont instanceof RClass ? (RClass)cont : null; } /** * Searched for method implementation recursively in current class and all * included modules, then if not found, in parent class and all included * modules, etc. * * @param app Rails app. * @param ctrlClass Class in which method implementation will be searched for. * @param methodName Name of the method to find. * @return RMethod object of null. */ @Nullable public static RMethod findControllerMethod(RailsApp app, @NotNull RClass ctrlClass, @NotNull String methodName) { RClass currentClass = ctrlClass; while (true) { RMethod method = RubyPsiUtil.getMethodWithPossibleZeroArgsByName(currentClass, methodName); if (method != null) return method; method = findMethodInClassModules(currentClass, methodName); if (method != null) return method; // Search in parent classes RSuperClass psiParentRef = currentClass.getPsiSuperClass(); if ((psiParentRef == null) || (psiParentRef.getName() == null)) return null; currentClass = findControllerClass(app, psiParentRef.getName()); if (currentClass == null) return null; } } /** * Performs search of specified class or module in IDE indexes. * * @param qualifiedName Full name of specified class or module controller * (with parent modules, ex. Devise::SessionsController) * @param project Current project. * @return RClass object, RModule object or null. */ @Nullable public static RContainer findClassOrModule(@NotNull String qualifiedName, @NotNull Project project) { // Search should be performed using only class name, without modules. // For example, if we have Devise::SessionsController, we should search // for only 'SessionsController' String[] classPath = qualifiedName.split("::"); String className = classPath[classPath.length - 1]; Collection items = findClassesAndModules(className, project); for (Object item: items) { String name = null; if (item instanceof RClass) name = ((RClass)item).getQualifiedName(); else if (item instanceof RModule) name = ((RModule)item).getQualifiedName(); // Perform case insensitive comparison to avoid mess with acronyms. if (qualifiedName.equalsIgnoreCase(name)) return (RContainer)item; } return null; } /** * Finds specified ruby class or module in IDE index. * * @param name Name of class or module to search for. This should be a name * without any modules, so if we wand to find * Devise::SessionsController, we should pass only * SessionsController here. * @param project Current project. * @return Collection of PSI elements which match specified name. */ @NotNull public static Collection findClassesAndModules(String name, Project project) { GlobalSearchScope scope = new RubyProjectAndLibrariesScope(project); // StubIndex.getElements was introduced in 134.231 build (RubyMine 6.3) return StubIndex.getElements(RubyClassModuleNameIndex.KEY, name, project, scope, RContainer.class); } public static String getControllerClassNameByShortName(String shortName) { return StringUtil.join(getControllerClassPathByShortName(shortName), "::"); } public static String[] getControllerClassPathByShortName(String shortName) { // Process namespaces String[] classPath = (shortName + "_controller").split("/"); for(int i = 0; i < classPath.length; i++) classPath[i] = NamingConventions.toCamelCase(classPath[i]); return classPath; } /** * Filter that selects only 'include Module::Name' expressions. */ private final static PsiElementFilter INCLUDE_MODULE_FILTER = psiElement -> (psiElement instanceof RCall) && ((RCall)psiElement).getCommand().equals("include"); /** * Performs search in all modules that are included in specified class. So * if ruby-class contains explicit includes: * * include Admin::MyModule * include Concerns::Logging * * the specified methodName will be searched in both modules in order * of their declaration. * * @param ctrlClass Class to look for modules. * @param methodName Method name to search within modules of specified ruby * class * @return First method which name matches methodName */ @Nullable private static RMethod findMethodInClassModules(RClass ctrlClass, String methodName) { PsiElement[] elements = PsiTreeUtil.collectElements(ctrlClass, INCLUDE_MODULE_FILTER); // Iterate from the end of the list as next included module can override // same-named methods of previously included module. int i = elements.length; while (--i >= 0) { RCall includeMethodCall = (RCall)elements[i]; RPsiElement moduleNameArg = includeMethodCall.getArguments().get(0); if (moduleNameArg == null) continue; RContainer cont = findClassOrModule(moduleNameArg.getText(), ctrlClass.getProject()); if (cont instanceof RModule) return RubyPsiUtil.getMethodWithPossibleZeroArgsByName(cont, methodName); } return null; } public static void logPsiParentChain(PsiElement elem) { while (elem != null) { if (elem instanceof PsiNamedElement) { System.out.println(elem.getClass().getName() + " --> Name: " + ((PsiNamedElement)elem).getName()); if (elem instanceof RClass) System.out.println(" ----- Class qualified name: " + ((RClass)elem).getQualifiedName()); } else System.out.println(elem.getClass().getName() + " --> No name"); elem = elem.getParent(); } } }