package com.cedricziel.idea.fluid.util; import com.cedricziel.idea.fluid.extensionPoints.VariableProvider; import com.cedricziel.idea.fluid.lang.psi.FluidFieldChain; import com.cedricziel.idea.fluid.lang.psi.FluidFieldChainExpr; import com.cedricziel.idea.fluid.lang.psi.FluidFieldExpr; import com.cedricziel.idea.fluid.lang.psi.FluidInlineChain; import com.cedricziel.idea.fluid.variables.FluidTypeContainer; import com.cedricziel.idea.fluid.variables.FluidVariable; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlTag; import com.intellij.util.PairProcessor; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.psi.elements.Field; import com.jetbrains.php.lang.psi.elements.Method; import com.jetbrains.php.lang.psi.elements.PhpClass; import com.jetbrains.php.lang.psi.elements.PhpNamedElement; import com.jetbrains.php.lang.psi.resolve.types.PhpType; import gnu.trove.THashMap; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.stream.Collectors; public class FluidTypeResolver { private static String[] PROPERTY_SHORTCUTS = new String[]{"get", "is", "has"}; public static Collection<String> formatPsiTypeName(@NotNull PsiElement psiElement) { return formatPsiTypeName(psiElement, false); } public static Collection<String> formatPsiTypeName(@NotNull PsiElement psiElement, boolean removeLast) { List<String> possibleTypes = new ArrayList<>(); if (psiElement.getPrevSibling() instanceof FluidInlineChain && psiElement.getPrevSibling().getFirstChild() != null && psiElement.getPrevSibling().getFirstChild() instanceof FluidFieldExpr) { possibleTypes.add(((FluidFieldExpr) psiElement.getPrevSibling().getFirstChild()).getName()); } else if (psiElement.getPrevSibling() instanceof FluidInlineChain && psiElement.getPrevSibling().getFirstChild() instanceof FluidFieldChainExpr) { psiElement = PsiTreeUtil.getDeepestLast(psiElement.getPrevSibling().getFirstChild()); } if (psiElement.getParent() instanceof FluidFieldChain) { FluidFieldChainExpr fieldExpression = (FluidFieldChainExpr) PsiTreeUtil.findFirstParent(psiElement, e -> e instanceof FluidFieldChainExpr); PsiTreeUtil.treeWalkUp(psiElement.getParent(), fieldExpression, new PairProcessor<PsiElement, PsiElement>() { @Override public boolean process(PsiElement psiElement, PsiElement psiElement2) { if (psiElement instanceof FluidFieldChainExpr) { FluidFieldExpr childOfType = PsiTreeUtil.findChildOfType(psiElement, FluidFieldExpr.class); if (childOfType != null) { possibleTypes.add(childOfType.getName()); } return false; } else { possibleTypes.add(((FluidFieldChain) psiElement).getName()); } return true; } }); } if (psiElement.getParent() instanceof FluidInlineChain) { // TODO } possibleTypes.sort(Collections.reverseOrder()); if (removeLast && possibleTypes.size() > 0) { possibleTypes.remove(possibleTypes.size() - 1); } return possibleTypes; } /** * Collects all possible variables in given path for last given item of "typeName" * * @param types Variable path "foo.bar" => ["foo", "bar"] * @return types for last item of typeName parameter */ @NotNull public static Collection<FluidTypeContainer> resolveFluidMethodName(@NotNull PsiElement psiElement, @NotNull Collection<String> types) { if (types.size() == 0) { return Collections.emptyList(); } String rootType = types.iterator().next(); Collection<FluidVariable> rootVariables = getRootVariableByName(psiElement, rootType); if (types.size() == 1) { Collection<FluidTypeContainer> fluidTypeContainers = FluidTypeContainer.fromCollection(psiElement.getProject(), rootVariables); return fluidTypeContainers; } Collection<FluidTypeContainer> type = FluidTypeContainer.fromCollection(psiElement.getProject(), rootVariables); Collection<List<FluidTypeContainer>> previousElements = new ArrayList<>(); previousElements.add(new ArrayList<>(type)); String[] typeNames = types.toArray(new String[0]); for (int i = 1; i <= typeNames.length - 1; i++) { type = resolveFluidMethodName(type, typeNames[i], previousElements); previousElements.add(new ArrayList<>(type)); // we can stop on empty list if (type.size() == 0) { return Collections.emptyList(); } } return type; } private static Collection<FluidVariable> getRootVariableByName(PsiElement psiElement, String rootType) { List<FluidVariable> variables = new ArrayList<>(); for (Map.Entry<String, FluidVariable> variable : collectScopeVariables(psiElement).entrySet()) { if (variable.getKey().equals(rootType)) { variables.add(variable.getValue()); } } return variables; } public static boolean isPropertyShortcutMethod(Method method) { for (String shortcut : PROPERTY_SHORTCUTS) { if (method.getName().startsWith(shortcut) && method.getName().length() > shortcut.length()) { return true; } } return false; } @NotNull public static String getPropertyShortcutMethodName(@NotNull Method method) { String methodName = method.getName(); for (String shortcut : PROPERTY_SHORTCUTS) { // strip possible property shortcut and make it lcfirst if (method.getName().startsWith(shortcut) && method.getName().length() > shortcut.length()) { methodName = methodName.substring(shortcut.length()); return Character.toLowerCase(methodName.charAt(0)) + methodName.substring(1); } } return methodName; } public static Collection<PhpClass> getClassFromPhpTypeSet(Project project, Set<String> types) { PhpType phpType = new PhpType(); for (String type : types) { phpType.add(type); } List<PhpClass> phpClasses = new ArrayList<>(); for (String typeName : PhpIndex.getInstance(project).completeType(project, phpType, new HashSet<>()).getTypes()) { if (typeName.startsWith("\\")) { PhpClass phpClass = getClassInterface(project, typeName); if (phpClass != null) { phpClasses.add(phpClass); } } } return phpClasses; } public static Collection<PhpClass> getClassFromPhpTypeSetArrayClean(Project project, Set<String> types) { PhpType phpType = new PhpType(); for (String type : types) { phpType.add(type); } ArrayList<PhpClass> phpClasses = new ArrayList<>(); for (String typeName : PhpIndex.getInstance(project).completeType(project, phpType, new HashSet<>()).getTypes()) { if (typeName.startsWith("\\")) { // we clean array types \Foo[] if (typeName.endsWith("[]")) { typeName = typeName.substring(0, typeName.length() - 2); } PhpClass phpClass = getClassInterface(project, typeName); if (phpClass != null) { phpClasses.add(phpClass); } } } return phpClasses; } @Nullable public static PhpClass getClassInterface(Project project, @NotNull String className) { Collection<PhpClass> phpClasses = PhpIndex.getInstance(project).getAnyByFQN(className); return phpClasses.size() == 0 ? null : phpClasses.iterator().next(); } public static Collection<PhpClass> getClassesInterface(Project project, @NotNull String className) { return PhpIndex.getInstance(project).getAnyByFQN(className); } /** * "phpNamedElement.variableName", "phpNamedElement.getVariableName" will resolve php type eg method * * @param phpNamedElement php class method or field * @param variableName variable name shortcut property possible * @return matched php types */ public static Collection<? extends PhpNamedElement> getFluidPhpNameTargets(PhpNamedElement phpNamedElement, String variableName) { Collection<PhpNamedElement> targets = new ArrayList<>(); if (phpNamedElement instanceof PhpClass) { for (Method method : ((PhpClass) phpNamedElement).getMethods()) { String methodName = method.getName(); if (method.getModifier().isPublic() && (methodName.equalsIgnoreCase(variableName) || isPropertyShortcutMethodEqual(methodName, variableName))) { targets.add(method); } } for (Field field : ((PhpClass) phpNamedElement).getFields()) { String fieldName = field.getName(); if (field.getModifier().isPublic() && fieldName.equalsIgnoreCase(variableName)) { targets.add(field); } } } return targets; } public static boolean isPropertyShortcutMethodEqual(String methodName, String variableName) { for (String shortcut : PROPERTY_SHORTCUTS) { if (methodName.equalsIgnoreCase(shortcut + variableName)) { return true; } } return false; } public static Map<String, FluidVariable> collectScopeVariables(PsiElement psiElement) { return collectScopeVariables(psiElement, new HashSet<>()); } @NotNull public static Map<String, FluidVariable> collectScopeVariables(@NotNull PsiElement psiElement, @NotNull Set<VirtualFile> visitedFiles) { Map<String, Set<String>> globalVars = new HashMap<>(); Map<String, FluidVariable> variables = new THashMap<>(); VirtualFile virtualFile = psiElement.getContainingFile().getVirtualFile(); if (visitedFiles.contains(virtualFile)) { return variables; } visitedFiles.add(virtualFile); for (VariableProvider extension : VariableProvider.EP_NAME.getExtensions()) { extension.provide(psiElement, variables); } for (Map.Entry<String, Set<String>> entry : globalVars.entrySet()) { Set<String> types = entry.getValue(); // collect iterator types.addAll(collectIteratorReturns(psiElement, entry.getValue())); // convert to variable model variables.put(entry.getKey(), new FluidVariable(types, null)); } // check if we are in a loop and resolve types ending with [] collectForArrayScopeVariables(psiElement, variables); return variables; } private static void collectForArrayScopeVariables(PsiElement psiElement, Map<String, FluidVariable> globalVars) { } @NotNull private static Set<String> collectIteratorReturns(@NotNull PsiElement psiElement, @NotNull Set<String> types) { Set<String> arrayValues = new HashSet<>(); for (String type : types) { PhpClass phpClass = getClassInterface(psiElement.getProject(), type); if (phpClass == null) { continue; } for (String methodName : new String[]{"getIterator", "__iterator", "current"}) { Method method = phpClass.findMethodByName(methodName); if (method != null) { // @method Foo __iterator // @method Foo[] __iterator Set<String> iteratorTypes = method.getType().getTypes(); if ("__iterator".equals(methodName) || "current".equals(methodName)) { arrayValues.addAll(iteratorTypes.stream().map(x -> !x.endsWith("[]") ? x + "[]" : x ).collect(Collectors.toSet())); } else { // Foobar[] for (String iteratorType : iteratorTypes) { if (iteratorType.endsWith("[]")) { arrayValues.add(iteratorType); } } } } } } return arrayValues; } private static Collection<FluidTypeContainer> resolveFluidMethodName(Collection<FluidTypeContainer> previousElement, String typeName, Collection<List<FluidTypeContainer>> twigTypeContainer) { ArrayList<FluidTypeContainer> phpNamedElements = new ArrayList<>(); for (FluidTypeContainer phpNamedElement : previousElement) { if (phpNamedElement.getPhpNamedElement() != null) { for (PhpNamedElement target : getFluidPhpNameTargets(phpNamedElement.getPhpNamedElement(), typeName)) { PhpType phpType = target.getType(); for (String typeString : phpType.getTypes()) { PhpNamedElement phpNamedElement1 = getClassInterface(phpNamedElement.getPhpNamedElement().getProject(), typeString); if (phpNamedElement1 != null) { phpNamedElements.add(new FluidTypeContainer(phpNamedElement1)); } } } } } return phpNamedElements; } public static Set<String> resolveFluidMethodName(Project project, Collection<String> previousElement, String typeName) { Set<String> types = new HashSet<>(); for (String prevClass : previousElement) { for (PhpClass phpClass : getClassesInterface(project, prevClass)) { for (PhpNamedElement target : getFluidPhpNameTargets(phpClass, typeName)) { types.addAll(target.getType().getTypes()); } } } return types; } public static String getTypeDisplayName(Project project, Set<String> types) { Collection<PhpClass> classFromPhpTypeSet = getClassFromPhpTypeSet(project, types); if (classFromPhpTypeSet.size() > 0) { return classFromPhpTypeSet.iterator().next().getPresentableFQN(); } PhpType phpType = new PhpType(); for (String type : types) { phpType.add(type); } PhpType phpTypeFormatted = PhpIndex.getInstance(project).completeType(project, phpType, new HashSet<>()); if (phpTypeFormatted.getTypes().size() > 0) { return StringUtils.join(phpTypeFormatted.getTypes(), "|"); } if (types.size() > 0) { return types.iterator().next(); } return ""; } /** * Get the "for IN" variable identifier as separated string * <p> * {% for car in "cars" %} * {% for car in "cars"|length %} * {% for car in "cars.test" %} */ @NotNull public static Collection<String> getForTagIdentifierAsString(XmlTag forTag) { XmlAttribute each = forTag.getAttribute("each"); if (each == null || each.getValueElement() == null) { return ContainerUtil.emptyList(); } PsiElement fluidElement = FluidUtil.retrieveFluidElementAtPosition(each.getValueElement()); if (fluidElement == null) { return Collections.emptyList(); } PsiElement deepestFirst = PsiTreeUtil.getDeepestFirst(fluidElement); return FluidTypeResolver.formatPsiTypeName(deepestFirst); } }