package fr.adrienbrault.idea.symfony2plugin.config.xml; import com.intellij.patterns.XmlPatterns; import com.intellij.psi.*; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlText; import com.intellij.psi.xml.XmlToken; import com.intellij.util.ProcessingContext; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.php.lang.psi.elements.Method; import com.jetbrains.php.lang.psi.elements.PhpClass; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.config.ClassPublicMethodReference; import fr.adrienbrault.idea.symfony2plugin.config.PhpClassReference; import fr.adrienbrault.idea.symfony2plugin.config.dic.EventDispatcherEventReference; import fr.adrienbrault.idea.symfony2plugin.config.xml.provider.ServiceReferenceProvider; import fr.adrienbrault.idea.symfony2plugin.dic.TagReference; import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collection; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class XmlReferenceContributor extends PsiReferenceContributor { @Override public void registerReferenceProviders(PsiReferenceRegistrar registrar) { // <argument type="service" id="service_container" /> registrar.registerReferenceProvider( XmlHelper.getArgumentServiceIdPattern(), new ServiceReferenceProvider() ); // <factory service="factory_service" /> registrar.registerReferenceProvider( XmlHelper.getFactoryServiceCompletionPattern(), new ServiceReferenceProvider() ); // <autowiring-type>Acme\TransformerInterface</autowiring-type> registrar.registerReferenceProvider( XmlHelper.getAutowiringTypePattern(), new PsiReferenceProvider() { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { if(!Symfony2ProjectComponent.isEnabled(psiElement)) { return new PsiReference[0]; } PsiElement parent = psiElement.getParent(); if(!(parent instanceof XmlText)) { return new PsiReference[0]; } String value = ((XmlText) parent).getValue(); return new PsiReference[]{ new PhpClassReference(psiElement, value) }; } } ); // <service class="%foo.class%"> // <service class="Class\Name"> registrar.registerReferenceProvider( XmlHelper.getServiceIdPattern(), new ClassPsiReferenceProvider() ); // <parameter key="fos_user.user_manager.class">FOS\UserBundle\Doctrine\UserManager</parameter> registrar.registerReferenceProvider( XmlHelper.getParameterClassValuePattern(), new PsiReferenceProvider() { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { if(!Symfony2ProjectComponent.isEnabled(psiElement)) { return new PsiReference[0]; } // get the service name "service_container" String text = psiElement.getText(); return new PsiReference[]{ new PhpClassReference(psiElement, text) }; } } ); // <argument>%form.resolved_type_factory.class%</argument> registrar.registerReferenceProvider( XmlHelper.getArgumentValuePattern(), new PsiReferenceProvider() { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { if(!Symfony2ProjectComponent.isEnabled(psiElement)) { return new PsiReference[0]; } PsiElement parent = psiElement.getParent(); if(!(parent instanceof XmlText)) { return new PsiReference[0]; } return new PsiReference[]{ new ParameterXmlReference(((XmlText) parent)) }; } } ); // <argument type="constant">Foobar\Foo</argument> registrar.registerReferenceProvider( XmlHelper.getArgumentValueWithTypePattern("constant"), new PsiReferenceProvider() { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { if(!Symfony2ProjectComponent.isEnabled(psiElement)) { return new PsiReference[0]; } PsiElement parent = psiElement.getParent(); if(!(parent instanceof XmlText)) { return new PsiReference[0]; } String text = parent.getText(); if(StringUtils.isBlank(text)) { return new PsiReference[0]; } return new PsiReference[]{ new ConstantXmlReference(((XmlText) parent)) }; } } ); // <tag name="kernel.event_subscriber" /> registrar.registerReferenceProvider( XmlHelper.getTagAttributePattern("tag", "name") .inside(XmlHelper.getInsideTagPattern("services")) .inFile(XmlHelper.getXmlFilePattern()), new PsiReferenceProvider() { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { if(!Symfony2ProjectComponent.isEnabled(element)) { return new PsiReference[0]; } if(element instanceof XmlAttributeValue) { return new PsiReference[] { new TagReference(element, PsiElementUtils.trimQuote(element.getText()))}; } return new PsiReference[0]; } } ); // <tag event="foo" method="kernel.event_subscriber" /> registrar.registerReferenceProvider( XmlHelper.getTagAttributePattern("tag", "method") .inside(XmlHelper.getInsideTagPattern("services")) .inFile(XmlHelper.getXmlFilePattern()), new ClassMethodReferenceProvider() ); registrar.registerReferenceProvider( XmlHelper.getTagAttributePattern("call", "method") .inside(XmlHelper.getInsideTagPattern("services")) .inFile(XmlHelper.getXmlFilePattern()), new ClassMethodReferenceProvider() ); // <factory class="AppBundle\Trivago\ConfigFactory"/> registrar.registerReferenceProvider( XmlHelper.getTagAttributePattern("factory", "class") .inFile(XmlHelper.getXmlFilePattern()), new ClassPsiReferenceProvider() ); // <factory class="AppBundle\Trivago\ConfigFactory" method="create"/> // <factory service="foo" method="create"/> registrar.registerReferenceProvider( XmlHelper.getTagAttributePattern("factory", "method") .inside(XmlHelper.getInsideTagPattern("services")) .inFile(XmlHelper.getXmlFilePattern()), new ChainPsiReferenceProvider( new FactoryClassMethodPsiReferenceProvider(), new FactoryServiceMethodPsiReferenceProvider() ) ); registrar.registerReferenceProvider( XmlHelper.getParameterWithClassEndingPattern() .inside(XmlHelper.getInsideTagPattern("parameters")) .inFile(XmlHelper.getXmlFilePattern() ), new PsiReferenceProvider() { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { if(!Symfony2ProjectComponent.isEnabled(element)) { return new PsiReference[0]; } if(element instanceof XmlToken) { return new PsiReference[] { new PhpClassReference(element, PsiElementUtils.removeIdeaRuleHack(PsiElementUtils.trimQuote(element.getText())), true) }; } return new PsiReference[0]; } } ); registrar.registerReferenceProvider( XmlHelper.getTagAttributePattern("tag", "event").inside(XmlHelper.getInsideTagPattern("services")), new PsiReferenceProvider() { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { if(!Symfony2ProjectComponent.isEnabled(element)) { return new PsiReference[0]; } return new PsiReference[] { new EventDispatcherEventReference(element, PsiElementUtils.removeIdeaRuleHack(PsiElementUtils.trimQuote(element.getText()))) }; } } ); } private static class ClassPsiReferenceProvider extends PsiReferenceProvider { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { if(!Symfony2ProjectComponent.isEnabled(psiElement) || !(psiElement instanceof XmlAttributeValue)) { return new PsiReference[0]; } return new PsiReference[]{ new ServiceIdReference( (XmlAttributeValue) psiElement) }; } } /** * <factory service="foo" method="create"/> */ private class FactoryServiceMethodPsiReferenceProvider extends PsiReferenceProvider { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext context) { if(!Symfony2ProjectComponent.isEnabled(psiElement) || !(psiElement instanceof XmlAttributeValue)) { return new PsiReference[0]; } String method = ((XmlAttributeValue) psiElement).getValue(); if(StringUtils.isBlank(method)) { return new PsiReference[0]; } PhpClass phpClass = XmlHelper.getPhpClassForServiceFactory((XmlAttributeValue) psiElement); if(phpClass == null) { return new PsiReference[0]; } Method targetMethod = phpClass.findMethodByName(method); if(targetMethod == null) { return new PsiReference[0]; } return new PsiReference[] { new ClassMethodStringPsiReference(psiElement, phpClass.getFQN(), targetMethod.getName()), }; } } private class ClassMethodReferenceProvider extends PsiReferenceProvider { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext context) { if(!Symfony2ProjectComponent.isEnabled(psiElement)) { return new PsiReference[0]; } // check for valid xml file and services container if(!XmlPatterns.psiElement().inside(XmlHelper.getInsideTagPattern("services")).inFile(XmlHelper.getXmlFilePattern()).accepts(psiElement)) { return new PsiReference[0]; } String serviceDefinitionClass = XmlHelper.getServiceDefinitionClass(psiElement); if(serviceDefinitionClass == null) { return new PsiReference[0]; } return new PsiReference[] { new ClassPublicMethodReference(psiElement, serviceDefinitionClass)}; } } private static class ServiceIdReference extends PsiPolyVariantReferenceBase<PsiElement> { private final XmlAttributeValue psiElement; public ServiceIdReference(XmlAttributeValue psiElement) { super(psiElement); this.psiElement = psiElement; } @NotNull @Override public ResolveResult[] multiResolve(boolean b) { String value = this.psiElement.getValue(); Collection<PsiElement> serviceClassTargets = ServiceUtil.getServiceClassTargets(getElement().getProject(), value); // @TODO: on implement multiple service resolve; we can make it nicer here // self add on compiler parameter, in this case we dont have a target; // to not get ide warnings if(serviceClassTargets.size() == 0 && value.startsWith("%") && value.endsWith("%") && value.length() > 2 && ContainerCollectionResolver.getParameterNames(getElement().getProject()).contains(StringUtils.strip(value, "%"))) { serviceClassTargets.add(getElement()); } return PsiElementResolveResult.createResults(serviceClassTargets); } @NotNull @Override public Object[] getVariants() { return new Object[0]; } } /** * <factory class="FooBar" method="cre<caret>ate"/> */ private class FactoryClassMethodPsiReferenceProvider extends PsiReferenceProvider { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { if(!Symfony2ProjectComponent.isEnabled(psiElement) || !(psiElement instanceof XmlAttributeValue)) { return new PsiReference[0]; } String method = ((XmlAttributeValue) psiElement).getValue(); if(StringUtils.isBlank(method)) { return new PsiReference[0]; } PhpClass phpClass = XmlHelper.getPhpClassForClassFactory((XmlAttributeValue) psiElement); if(phpClass == null) { return new PsiReference[0]; } Method classMethod = phpClass.findMethodByName(method); if(classMethod == null) { return new PsiReference[0]; } return new PsiReference[]{ new ClassMethodStringPsiReference(psiElement, phpClass.getFQN(), classMethod.getName()), }; } } private class ChainPsiReferenceProvider extends PsiReferenceProvider { @NotNull private final PsiReferenceProvider[] psiReferenceProviders; ChainPsiReferenceProvider(@NotNull PsiReferenceProvider... psiReferenceProviders) { this.psiReferenceProviders = psiReferenceProviders; } @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) { Collection<PsiReference> psiReferences = new ArrayList<>(); for (PsiReferenceProvider provider : this.psiReferenceProviders) { ContainerUtil.addAll(psiReferences, provider.getReferencesByElement(psiElement, processingContext)); } return psiReferences.toArray(new PsiReference[psiReferences.size()]); } } private class ClassMethodStringPsiReference extends PsiPolyVariantReferenceBase<PsiElement> { @NotNull private final String aClass; @NotNull private final String method; ClassMethodStringPsiReference(@NotNull PsiElement psiElement, @NotNull String aClass, @NotNull String method) { super(psiElement); this.aClass = aClass; this.method = method; } @NotNull @Override public ResolveResult[] multiResolve(boolean b) { Method classMethod = PhpElementsUtil.getClassMethod(getElement().getProject(), aClass, method); if(classMethod == null) { return new ResolveResult[0]; } return PsiElementResolveResult.createResults(classMethod); } @NotNull @Override public Object[] getVariants() { return new Object[0]; } } }