package fr.adrienbrault.idea.symfony2plugin.form.util; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.project.Project; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.psi.elements.*; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.form.dict.*; import fr.adrienbrault.idea.symfony2plugin.form.visitor.FormOptionLookupVisitor; import fr.adrienbrault.idea.symfony2plugin.form.visitor.FormOptionVisitor; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.stream.Collectors; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class FormOptionsUtil { public static final String EXTENDED_TYPE_METHOD = "getExtendedType"; /** * Symfony2 / 3 Form setting options per FormType via method */ public static final String[] FORM_OPTION_METHODS = { "setDefaultOptions", "configureOptions" }; /** * Methods which provides possible form options on first parameter */ private static final String[] OPTIONS_VIA_METHOD_PARAMETER = { "setRequired", "setOptional", "setDefined", "setDefault", "setAllowedValues", "addAllowedValues", "setAllowedTypes", "addAllowedTypes" }; /** * Find form extensions extends given form type * * @param formTypeNames Fqn class "Foo\Foo", "\Foo\Foo" or string value "foo"; * should have all parents a FormType can have */ @NotNull public static Collection<FormClass> getExtendedTypeClasses(@NotNull Project project, @NotNull String... formTypeNames) { // strip "\" List<String> formTypeNamesList = ContainerUtil.map(Arrays.asList(formTypeNames), s -> StringUtils.stripStart(s, "\\") ); Collection<FormClass> extendedTypeClasses = new ArrayList<>(); for(PhpClass phpClass: getFormTypeExtensionClassNames(project)) { String formExtendedType = FormUtil.getFormExtendedType(phpClass); if(formExtendedType != null && formTypeNamesList.contains(formExtendedType)) { extendedTypeClasses.add(new FormClass(FormClassEnum.EXTENSION, phpClass, true)); } } return extendedTypeClasses; } @NotNull private static Set<PhpClass> getFormTypeExtensionClassNames(@NotNull Project project) { Set<PhpClass> phpClasses = new HashSet<>(); // @TODO: should be same as interface? for (String s : ServiceXmlParserFactory.getInstance(project, FormExtensionServiceParser.class).getFormExtensions().keySet()) { ContainerUtil.addIfNotNull( phpClasses, PhpElementsUtil.getClass(project, s) ); } for(PhpClass phpClass: PhpIndex.getInstance(project).getAllSubclasses(FormUtil.FORM_EXTENSION_INTERFACE)) { if(!FormUtil.isValidFormPhpClass(phpClass)) { continue; } phpClasses.add(phpClass); } return phpClasses; } @NotNull public static Map<String, FormOption> getFormExtensionKeys(@NotNull Project project, @NotNull String... formTypeNames) { Collection<FormClass> typeClasses = FormOptionsUtil.getExtendedTypeClasses(project, formTypeNames); Map<String, FormOption> extensionClassMap = new HashMap<>(); for(FormClass extensionClass: typeClasses) { extensionClassMap.putAll(getDefaultOptions(project, extensionClass.getPhpClass(), extensionClass)); } return extensionClassMap; } /** * finishView, buildView: * $this->vars */ public static Set<String> getFormViewVars(Project project, String... formTypeNames) { Set<String> stringSet = new HashSet<>(); Set<String> uniqueClass = new HashSet<>(); List<PhpClass> phpClasses = new ArrayList<>(); // attach core form phpclass // @TODO: add formtype itself PhpClass coreForm = FormUtil.getFormTypeToClass(project, "form"); if(coreForm != null) { phpClasses.add(coreForm); uniqueClass.add(coreForm.getPresentableFQN()); } // for extension can also provide vars for(FormOption entry: FormOptionsUtil.getFormExtensionKeys(project, formTypeNames).values()) { PhpClass phpClass = entry.getFormClass().getPhpClass(); if(!uniqueClass.contains(phpClass.getPresentableFQN())) { phpClasses.add(phpClass); } } for(PhpClass phpClass: phpClasses) { for(String stringMethod: new String[] {"finishView", "buildView"} ) { Method method = phpClass.findMethodByName(stringMethod); if(method != null) { // self method getMethodVars(stringSet, method); // allow parent:: // @TODO: provide global util method for(ClassReference classReference : PsiTreeUtil.collectElementsOfType(method, ClassReference.class)) { if("parent".equals(classReference.getName())) { PsiElement methodReference = classReference.getContext(); if(methodReference instanceof MethodReference) { PsiElement parentMethod = ((MethodReference) methodReference).resolve(); if(parentMethod instanceof Method) { getMethodVars(stringSet, (Method) parentMethod); } } } } } } } return stringSet; } private static void getMethodVars(Set<String> stringSet, Method method) { Collection<FieldReference> fieldReferences = PsiTreeUtil.collectElementsOfType(method, FieldReference.class); for(FieldReference fieldReference: fieldReferences) { PsiElement psiVar = PsiElementUtils.getChildrenOfType(fieldReference, PlatformPatterns.psiElement().withText("vars")); if(psiVar != null) { getFormViewVarsAttachKeys(stringSet, fieldReference); } } } /** * $this->vars['test'] * $view->vars = array_replace($view->vars, array(...)); */ private static void getFormViewVarsAttachKeys(Set<String> stringSet, FieldReference fieldReference) { // $this->vars['test'] PsiElement context = fieldReference.getContext(); if(context instanceof ArrayAccessExpression) { ArrayIndex arrayIndex = PsiTreeUtil.findChildOfType(context, ArrayIndex.class); if(arrayIndex != null) { PsiElement psiElement = arrayIndex.getFirstChild(); if(psiElement instanceof StringLiteralExpression) { String contents = ((StringLiteralExpression) psiElement).getContents(); if(StringUtils.isNotBlank(contents)) { stringSet.add(contents); } } } } // array_replace($view->vars, array(...)) if(context instanceof ParameterList) { PsiElement functionReference = context.getContext(); if(functionReference instanceof FunctionReference && "array_replace".equals(((FunctionReference) functionReference).getName())) { PsiElement[] psiElements = ((ParameterList) context).getParameters(); if(psiElements.length > 1) { if(psiElements[1] instanceof ArrayCreationExpression) { stringSet.addAll(PhpElementsUtil.getArrayCreationKeys((ArrayCreationExpression) psiElements[1])); } } } } } @Deprecated public static Map<String, String> getFormDefaultKeys(@NotNull Project project, @NotNull String formTypeName) { final Map<String, String> items = new HashMap<>(); getFormDefaultKeys(project, formTypeName, new HashMap<>(), new FormUtil.FormTypeCollector(project).collect(), 0, (psiElement, option, formClass, optionEnum) -> { String presentableFQN = formClass.getPhpClass().getPresentableFQN(); items.put(option, presentableFQN); }); return items; } public static void visitFormOptions(@NotNull Project project, @NotNull String formTypeName, @NotNull FormOptionVisitor visitor) { visitFormOptions(project, formTypeName, new HashMap<>(), new FormUtil.FormTypeCollector(project).collect(), 0, visitor); } private static Map<String, String> visitFormOptions(Project project, String formTypeName, HashMap<String, String> defaultValues, FormUtil.FormTypeCollector collector, int depth, @NotNull FormOptionVisitor visitor) { PhpClass phpClass = collector.getFormTypeToClass(formTypeName); if(phpClass == null) { return defaultValues; } getDefaultOptions(project, phpClass, new FormClass(FormClassEnum.FORM_TYPE, phpClass, false), visitor); for (FormClass formClass : getExtendedTypeClasses(project, formTypeName)) { getDefaultOptions(project, formClass.getPhpClass(), new FormClass(FormClassEnum.EXTENSION, formClass.getPhpClass(), false), visitor); } // recursive search for parent form types if (depth < 10) { String formParent = FormUtil.getFormParentOfPhpClass(phpClass); if(formParent != null) { visitFormOptions(project, formParent, defaultValues, collector, ++depth, visitor); } } return defaultValues; } public static void getFormDefaultKeys(@NotNull Project project, @NotNull String formTypeName, @NotNull FormOptionVisitor visitor) { getFormDefaultKeys(project, formTypeName, new HashMap<>(), new FormUtil.FormTypeCollector(project).collect(), 0, visitor); } private static Map<String, String> getFormDefaultKeys(Project project, String formTypeName, HashMap<String, String> defaultValues, FormUtil.FormTypeCollector collector, int depth, @NotNull FormOptionVisitor visitor) { PhpClass phpClass = collector.getFormTypeToClass(formTypeName); if(phpClass == null) { return defaultValues; } getDefaultOptions(project, phpClass, new FormClass(FormClassEnum.FORM_TYPE, phpClass, false), visitor); // recursive search for parent form types if (depth < 10) { String formParent = FormUtil.getFormParentOfPhpClass(phpClass); if(formParent != null) { getFormDefaultKeys(project, formParent, defaultValues, collector, ++depth, visitor); } } return defaultValues; } @NotNull private static Map<String, FormOption> getDefaultOptions(@NotNull Project project, @NotNull PhpClass phpClass, @NotNull FormClass formClass) { final Map<String, FormOption> options = new HashMap<>(); getDefaultOptions(project, phpClass, formClass, (psiElement, option, formClass1, optionEnum) -> { // append REQUIRED, if we already know this value if(options.containsKey(option)) { options.get(option).addOptionEnum(optionEnum); } else { options.put(option, new FormOption(option, formClass1, optionEnum)); } }); return options; } private static void getDefaultOptions(@NotNull Project project, @NotNull PhpClass phpClass, @NotNull FormClass formClass, @NotNull FormOptionVisitor visitor) { for(String methodName: FORM_OPTION_METHODS) { Method method = phpClass.findMethodByName(methodName); if(method == null) { continue; } Collection<MethodReference> tests = PsiTreeUtil.findChildrenOfType(method, MethodReference.class); for(MethodReference methodReference: tests) { if(PhpElementsUtil.isEqualMethodReferenceName(methodReference, "setDefaults")) { PsiElement[] parameters = methodReference.getParameters(); if(parameters.length > 0 && parameters[0] instanceof ArrayCreationExpression) { for(Map.Entry<String, PsiElement> entry: PhpElementsUtil.getArrayCreationKeyMap((ArrayCreationExpression) parameters[0]).entrySet()) { visitor.visit(entry.getValue(), entry.getKey(), formClass, FormOptionEnum.DEFAULT); } } } else { // ->setRequired(['test', 'test2']) for(String currentMethod: OPTIONS_VIA_METHOD_PARAMETER) { if(PhpElementsUtil.isEqualMethodReferenceName(methodReference, currentMethod)) { PsiElement[] parameters = methodReference.getParameters(); if(parameters.length > 0 && parameters[0] instanceof ArrayCreationExpression) { for (Map.Entry<String, PsiElement> entry : PhpElementsUtil.getArrayValuesAsMap((ArrayCreationExpression) parameters[0]).entrySet()) { visitor.visit(entry.getValue(), entry.getKey(), formClass, FormOptionEnum.getEnum(currentMethod)); } } break; } } } // support: parent::setDefaultOptions($resolver) // Symfony\Component\Form\Extension\Core\Type\FormType:setDefaultOptions if(PhpElementsUtil.isEqualMethodReferenceName(methodReference, methodName) && methodReference.getReferenceType() == PhpModifier.State.PARENT) { PsiElement parentMethod = PhpElementsUtil.getPsiElementsBySignatureSingle(project, methodReference.getSignature()); if(parentMethod instanceof Method) { PhpClass phpClassInner = ((Method) parentMethod).getContainingClass(); if(phpClassInner != null) { // @TODO only use setDefaultOptions, recursive call get setDefaults again getDefaultOptions(project, phpClassInner, formClass, visitor); } } } } } } /** * Build completion lookup element for form options * Reformat class name to make it more readable * * @param formOption Extension or a default option * @return lookup element */ public static LookupElement getOptionLookupElement(FormOption formOption) { String typeText = formOption.getFormClass().getPhpClass().getPresentableFQN(); if(typeText.lastIndexOf("\\") != -1) { typeText = typeText.substring(typeText.lastIndexOf("\\") + 1); if(typeText.endsWith("Extension")) { typeText = typeText.substring(0, typeText.length() - 9); } } return LookupElementBuilder.create(formOption.getOption()) .withTypeText(typeText, true) .withIcon(formOption.getFormClass().isWeak() ? Symfony2Icons.FORM_EXTENSION_WEAK : Symfony2Icons.FORM_EXTENSION); } @NotNull public static Collection<PsiElement> getFormExtensionsKeysTargets(@NotNull StringLiteralExpression psiElement, String... formTypes) { Map<String, FormOption> extensionKeys = FormOptionsUtil.getFormExtensionKeys(psiElement.getProject(), formTypes); String value = psiElement.getContents(); if(!extensionKeys.containsKey(value)) { return Collections.emptyList(); } Collection<PsiElement> psiElements = new HashSet<>(); PhpClass phpClass = extensionKeys.get(value).getFormClass().getPhpClass(); // Symfony <= 2.7 and > 2.7 api level search for (String methodName : FORM_OPTION_METHODS) { Method method = phpClass.findMethodByName(methodName); if(method == null) { continue; } ContainerUtil.addIfNotNull( psiElements, PhpElementsUtil.findArrayKeyValueInsideReference(method, "setDefaults", value) ); } return psiElements; } public static Collection<LookupElement> getFormExtensionKeysLookupElements(Project project, String... formTypes) { return FormOptionsUtil.getFormExtensionKeys(project, formTypes).values().stream() .map(FormOptionsUtil::getOptionLookupElement) .collect(Collectors.toCollection(ArrayList::new)); } @Deprecated @NotNull public static Collection<PsiElement> getDefaultOptionTargets(@NotNull StringLiteralExpression element, @NotNull String formType) { final String value = element.getContents(); if(StringUtils.isBlank(value)) { return Collections.emptySet(); } final Collection<PsiElement> psiElements = new ArrayList<>(); FormOptionsUtil.getFormDefaultKeys(element.getProject(), formType, (psiElement, option, formClass, optionEnum) -> { if(option.equals(value)) { psiElements.add(psiElement); } }); return psiElements; } @NotNull public static Collection<LookupElement> getDefaultOptionLookupElements(@NotNull Project project, @NotNull String formType) { Collection<LookupElement> lookupElements = new ArrayList<>(); FormOptionsUtil.getFormDefaultKeys(project, formType, new FormOptionLookupVisitor(lookupElements)); return lookupElements; } @Nullable public static String getTranslationFromScope(@NotNull ArrayCreationExpression arrayCreation) { // translation_domain in current array block String translationDomain = PhpElementsUtil.getArrayHashValue(arrayCreation, "translation_domain"); if(translationDomain == null) { // find on default options inside FormType translationDomain = PhpElementsUtil.getArrayKeyValueInsideSignature(arrayCreation, FormOptionsUtil.FORM_OPTION_METHODS, "setDefaults", "translation_domain"); } return translationDomain; } }