package fr.adrienbrault.idea.symfony2plugin.config.xml; import com.intellij.patterns.*; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.source.xml.XmlDocumentImpl; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.*; import com.intellij.util.Consumer; import com.jetbrains.php.lang.psi.elements.Parameter; import com.jetbrains.php.lang.psi.elements.PhpClass; import fr.adrienbrault.idea.symfony2plugin.dic.ParameterResolverConsumer; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil; import fr.adrienbrault.idea.symfony2plugin.util.yaml.visitor.ParameterVisitor; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class XmlHelper { public static PsiElementPattern.Capture<PsiElement> getTagPattern(String... tags) { return XmlPatterns .psiElement() .inside(XmlPatterns .xmlAttributeValue() .inside(XmlPatterns .xmlAttribute() .withName(StandardPatterns.string().oneOfIgnoreCase(tags) ) ) ); } /** * <tag attributeNames="|"/> * * @param tag tagname * @param attributeNames attribute values listen for */ public static PsiElementPattern.Capture<PsiElement> getTagAttributePattern(String tag, String... attributeNames) { return XmlPatterns .psiElement() .inside(XmlPatterns .xmlAttributeValue() .inside(XmlPatterns .xmlAttribute() .withName(StandardPatterns.string().oneOfIgnoreCase(attributeNames)) .withParent(XmlPatterns .xmlTag() .withName(tag) ) ) ).inFile(getXmlFilePattern()); } /** * <tag attributeNames="|"/> * * @param attributeNames attribute values listen for */ public static PsiElementPattern.Capture<PsiElement> getAttributePattern(String... attributeNames) { return XmlPatterns .psiElement() .inside(XmlPatterns .xmlAttributeValue() .inside(XmlPatterns .xmlAttribute() .withName(StandardPatterns.string().oneOfIgnoreCase(attributeNames)) ) ).inFile(getXmlFilePattern()); } /** * <tag attributeNames="|"/> */ public static PsiElementPattern.Capture<PsiElement> getGlobalStringAttributePattern() { return XmlPatterns .psiElement() .inside(XmlPatterns .xmlAttributeValue().withValue(XmlPatterns.string().andOr( XmlPatterns.string().endsWith(".html.twig"), XmlPatterns.string().endsWith(".html.php") )) ).inFile(getXmlFilePattern()); } /** * <parameter key="fos_user.user_manager.class">FOS\UserBundle\Doctrine\UserManager</parameter> */ public static PsiElementPattern.Capture<PsiElement> getParameterWithClassEndingPattern() { return XmlPatterns .psiElement() .withParent(XmlPatterns .xmlText() .withParent(XmlPatterns .xmlTag() .withName("parameter").withChild( XmlPatterns.xmlAttribute("key").withValue( XmlPatterns.string().endsWith(".class") ) ) ) ).inside( XmlPatterns.psiElement(XmlTag.class).withName("parameters") ).inFile(getXmlFilePattern()); } /** * <argument type="service" id="service_container" /> */ public static XmlAttributeValuePattern getArgumentServiceIdPattern() { return XmlPatterns .xmlAttributeValue() .withParent(XmlPatterns .xmlAttribute("id") .withParent(XmlPatterns .xmlTag() .withChild(XmlPatterns .xmlAttribute("type") .withValue( StandardPatterns.string().equalTo("service") ) ) ) ).inside( XmlHelper.getInsideTagPattern("services") ).inFile(XmlHelper.getXmlFilePattern()); } /** * Possible service id completion on not ready type="service" argument * * <service><argument id="service_container" /></service> */ public static XmlAttributeValuePattern getArgumentServiceIdForArgumentPattern() { return XmlPatterns .xmlAttributeValue() .withParent(XmlPatterns .xmlAttribute("id") .withParent(XmlPatterns .xmlTag().withParent(XmlPatterns .xmlTag().withName("service")) ) ).inside( XmlHelper.getInsideTagPattern("services") ).inFile(XmlHelper.getXmlFilePattern()); } /** * <service id="service_container" /> */ public static XmlAttributeValuePattern getServiceIdNamePattern() { return XmlPatterns .xmlAttributeValue() .withParent(XmlPatterns .xmlAttribute("id") .withParent(XmlPatterns .xmlTag() .withName("service") ) ).inside( XmlHelper.getInsideTagPattern("services") ).inFile(XmlHelper.getXmlFilePattern()); } /** * <factory service="factory_service" method="createFooMethod" /> */ public static XmlAttributeValuePattern getFactoryServiceCompletionPattern() { return XmlPatterns .xmlAttributeValue() .withParent(XmlPatterns .xmlAttribute("service") .withParent(XmlPatterns .xmlTag().withName("factory") ) ).inside( XmlHelper.getInsideTagPattern("services") ).inFile(XmlHelper.getXmlFilePattern()); } /** * <parameter key="fos_user.user_manager.class">FOS\UserBundle\Doctrine\UserManager</parameter> */ public static PsiElementPattern.Capture<PsiElement> getParameterClassValuePattern() { // @TODO: check attribute value ends with ".class" return XmlPatterns .psiElement(XmlTokenType.XML_DATA_CHARACTERS) .withText(StandardPatterns.string().contains("\\")) .withParent(XmlPatterns .xmlText() .withParent(XmlPatterns .xmlTag() .withName("parameter") .withAnyAttribute("key") ).inside( XmlHelper.getInsideTagPattern("services") ) ).inFile(XmlHelper.getXmlFilePattern()); } /** * <argument>%form.resolved_type_factory.class%</argument> */ public static PsiElementPattern.Capture<PsiElement> getArgumentValuePattern() { return XmlPatterns .psiElement(XmlTokenType.XML_DATA_CHARACTERS) .withParent(XmlPatterns .xmlText() .withParent(XmlPatterns .xmlTag() .withName("argument") ) ).inside( XmlHelper.getInsideTagPattern("services") ).inFile(XmlHelper.getXmlFilePattern()); } /** * <argument type="constant">Foo\Bar::CONST</argument> */ public static PsiElementPattern.Capture<PsiElement> getArgumentValueWithTypePattern(@NotNull String type) { return XmlPatterns .psiElement(XmlTokenType.XML_DATA_CHARACTERS) .withParent(XmlPatterns .xmlText() .withParent(XmlPatterns .xmlTag() .withName("argument") .withAttributeValue("type", type) ) ).inside( XmlHelper.getInsideTagPattern("services") ).inFile(XmlHelper.getXmlFilePattern()); } /** * <autowiring-type>Foo\Class</autowiring-type> */ public static PsiElementPattern.Capture<PsiElement> getAutowiringTypePattern() { return XmlPatterns .psiElement(XmlTokenType.XML_DATA_CHARACTERS) .withParent(XmlPatterns .xmlText() .withParent(XmlPatterns .xmlTag() .withName("autowiring-type") ) ).inside( XmlHelper.getInsideTagPattern("services") ).inFile(XmlHelper.getXmlFilePattern()); } /** * <service class="%foo.class%"> */ public static XmlAttributeValuePattern getServiceIdPattern() { return XmlPatterns .xmlAttributeValue() .withParent(XmlPatterns .xmlAttribute("class") .withParent(XmlPatterns .xmlTag() .withChild( XmlPatterns.xmlAttribute("id") ) ) ).inside( XmlHelper.getInsideTagPattern("services") ).inFile(XmlHelper.getXmlFilePattern()); } public static PsiFilePattern.Capture<PsiFile> getXmlFilePattern() { return XmlPatterns.psiFile() .withName(XmlPatterns .string().endsWith(".xml") ); } /** * <import> * <resource="@Foo"/> * </import> */ public static XmlAttributeValuePattern getImportResourcePattern() { return XmlPatterns .xmlAttributeValue() .withParent(XmlPatterns .xmlAttribute("resource") .withParent(XmlPatterns .xmlTag().withName("import") ) ); } public static PsiElementPattern.Capture<XmlTag> getInsideTagPattern(String insideTagName) { return XmlPatterns.psiElement(XmlTag.class).withName(insideTagName); } public static PsiElementPattern.Capture<XmlTag> getInsideTagPattern(String... insideTagName) { return XmlPatterns.psiElement(XmlTag.class).withName(XmlPatterns.string().oneOf(insideTagName)); } /** * <route id="foo" path="/blog/{slug}"> * <default key="_controller">Foo:Demo:hello</default> * </route> */ public static PsiElementPattern.Capture<PsiElement> getRouteDefaultWithKeyAttributePattern(@NotNull String key) { return XmlPatterns .psiElement(XmlTokenType.XML_DATA_CHARACTERS) .withParent(XmlPatterns .xmlText() .withParent(XmlPatterns .xmlTag() .withName("default") .withChild( XmlPatterns.xmlAttribute().withName("key").withValue( XmlPatterns.string().oneOfIgnoreCase(key) ) ) ) ).inside( XmlHelper.getInsideTagPattern("route") ).inFile(XmlHelper.getXmlFilePattern()); } @Nullable public static PsiElement getLocalServiceName(PsiFile psiFile, String serviceName) { if(!(psiFile.getFirstChild() instanceof XmlDocument)) { return null; } XmlTag xmlTags[] = PsiTreeUtil.getChildrenOfType(psiFile.getFirstChild(), XmlTag.class); if(xmlTags == null) { return null; } for(XmlTag xmlTag: xmlTags) { if(xmlTag.getName().equals("container")) { for(XmlTag servicesTag: xmlTag.getSubTags()) { if(servicesTag.getName().equals("services")) { for(XmlTag serviceTag: servicesTag.getSubTags()) { String serviceNameId = serviceTag.getAttributeValue("id"); if(serviceNameId != null && serviceNameId.equalsIgnoreCase(serviceName)) { return serviceTag; } } } } } } return null; } @Nullable public static PsiElement getLocalParameterName(PsiFile psiFile, String serviceName) { if(!(psiFile.getFirstChild() instanceof XmlDocumentImpl)) { return null; } XmlTag xmlTags[] = PsiTreeUtil.getChildrenOfType(psiFile.getFirstChild(), XmlTag.class); if(xmlTags == null) { return null; } for(XmlTag xmlTag: xmlTags) { if(xmlTag.getName().equals("container")) { for(XmlTag servicesTag: xmlTag.getSubTags()) { if(servicesTag.getName().equals("parameters")) { for(XmlTag serviceTag: servicesTag.getSubTags()) { XmlAttribute attrValue = serviceTag.getAttribute("key"); if(attrValue != null) { String serviceNameId = attrValue.getValue(); if(serviceNameId != null && serviceNameId.equals(serviceName)) { return serviceTag; } } } } } } } return null; } public static Map<String, String> getFileParameterMap(XmlFile psiFile) { Map<String, String> services = new HashMap<>(); if(!(psiFile.getFirstChild() instanceof XmlDocumentImpl)) { return services; } XmlTag xmlTags[] = PsiTreeUtil.getChildrenOfType(psiFile.getFirstChild(), XmlTag.class); if(xmlTags == null) { return services; } for(XmlTag xmlTag: xmlTags) { if(xmlTag.getName().equals("container")) { for(XmlTag servicesTag: xmlTag.getSubTags()) { if(servicesTag.getName().equals("parameters")) { for(XmlTag parameterTag: servicesTag.getSubTags()) { // <parameter key="fos_user.user_manager.class">FOS\UserBundle\Doctrine\UserManager</parameter> // <parameter key="fos_rest.formats" type="collection"> // <parameter key="json">false</parameter> // </parameter> if(parameterTag.getName().equals("parameter")) { XmlAttribute keyAttr = parameterTag.getAttribute("key"); if(keyAttr != null) { String parameterName = keyAttr.getValue(); if(parameterName != null && StringUtils.isNotBlank(parameterName)) { String parameterValue = null; String typeAttr = parameterTag.getAttributeValue("type"); // get value of parameter if we have a text value if(!"collection".equals(typeAttr) && parameterTag.getSubTags().length == 0) { XmlTagValue attrClass = parameterTag.getValue(); String myParameterValue = attrClass.getText(); // dont index long values if(myParameterValue.length() < 150) { parameterValue = myParameterValue; } } services.put(parameterName.toLowerCase(), parameterValue); } } } } } } } } return services; } /** * Get class attribute from service on every inside element * * @param psiInsideService every PsiElement inside service * @return raw class attribute value */ @Nullable public static String getServiceDefinitionClass(PsiElement psiInsideService) { // search for parent service definition XmlTag callXmlTag = PsiTreeUtil.getParentOfType(psiInsideService, XmlTag.class); XmlTag xmlTag = PsiTreeUtil.getParentOfType(callXmlTag, XmlTag.class); if(xmlTag == null || !xmlTag.getName().equals("service")) { return null; } XmlAttribute classAttribute = xmlTag.getAttribute("class"); if(classAttribute == null) { return null; } String value = classAttribute.getValue(); if(StringUtils.isNotBlank(value)) { return value; } return null; } @Nullable public static PhpClass getPhpClassForClassFactory(@NotNull XmlAttributeValue xmlAttributeValue) { String method = xmlAttributeValue.getValue(); if(StringUtils.isBlank(method)) { return null; } XmlTag parentOfType = PsiTreeUtil.getParentOfType(xmlAttributeValue, XmlTag.class); if(parentOfType == null) { return null; } String aClass = parentOfType.getAttributeValue("class"); if(aClass == null || StringUtils.isBlank(aClass)) { return null; } return PhpElementsUtil.getClass(xmlAttributeValue.getProject(), aClass); } @Nullable public static PhpClass getPhpClassForServiceFactory(@NotNull XmlAttributeValue xmlAttributeValue) { String method = xmlAttributeValue.getValue(); if(StringUtils.isBlank(method)) { return null; } XmlTag callXmlTag = PsiTreeUtil.getParentOfType(xmlAttributeValue, XmlTag.class); if(callXmlTag == null) { return null; } String service = callXmlTag.getAttributeValue("service"); if(StringUtils.isBlank(service)) { return null; } return ServiceUtil.getResolvedClassDefinition(xmlAttributeValue.getProject(), service); } /** * <service id="app.newsletter_manager" class="AppBundle\Mail\NewsletterManager"> * <call method="setMailer"> * <argument type="service" id="mai<caret>ler" /> * </call> * </service> */ public static void visitServiceCallArgument(@NotNull XmlAttributeValue xmlAttributeValue, @NotNull Consumer<ParameterVisitor> consumer) { // search for parent service definition PsiElement xmlAttribute = xmlAttributeValue.getParent(); if(xmlAttribute instanceof XmlAttribute) { PsiElement xmlArgumentTag = xmlAttribute.getParent(); if(xmlArgumentTag instanceof XmlTag) { PsiElement xmlCallTag = xmlArgumentTag.getParent(); if(xmlCallTag instanceof XmlTag) { String name = ((XmlTag) xmlCallTag).getName(); if (name.equals("call")) { // service/call/argument[id] XmlAttribute methodAttribute = ((XmlTag) xmlCallTag).getAttribute("method"); if(methodAttribute != null) { String methodName = methodAttribute.getValue(); if(methodName != null) { XmlTag serviceTag = ((XmlTag) xmlCallTag).getParentTag(); // get service class if(serviceTag != null && "service".equals(serviceTag.getName())) { XmlAttribute classAttribute = serviceTag.getAttribute("class"); if(classAttribute != null) { String className = classAttribute.getValue(); if(className != null) { consumer.consume(new ParameterVisitor( className, methodName, XmlServiceContainerAnnotator.getArgumentIndex((XmlTag) xmlArgumentTag)) ); } } } } } } } } } } /** * Consumer for method parameter match * * service_name: * class: FOOBAR * calls: * - [onFoobar, [@fo<caret>o]] */ public static void visitServiceCallArgumentMethodIndex(@NotNull XmlAttributeValue xmlAttribute, @NotNull Consumer<Parameter> consumer) { visitServiceCallArgument(xmlAttribute, new ParameterResolverConsumer(xmlAttribute.getProject(), consumer)); } }