package fr.adrienbrault.idea.symfony2plugin.config.xml;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.Consumer;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.php.lang.psi.elements.Method;
import com.jetbrains.php.lang.psi.elements.Parameter;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.dic.ContainerService;
import fr.adrienbrault.idea.symfony2plugin.intentions.ui.ServiceSuggestDialog;
import fr.adrienbrault.idea.symfony2plugin.intentions.xml.XmlServiceSuggestIntention;
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
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.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class XmlServiceContainerAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement psiElement, @NotNull AnnotationHolder holder) {
if(!Symfony2ProjectComponent.isEnabled(psiElement.getProject())) {
return;
}
annotateServiceInstance(psiElement, holder);
}
private void annotateServiceInstance(@NotNull PsiElement psiElement, @NotNull AnnotationHolder holder) {
if(!XmlHelper.getArgumentServiceIdPattern().accepts(psiElement)) {
return;
}
// search for parent service definition
XmlTag currentXmlTag = PsiTreeUtil.getParentOfType(psiElement, XmlTag.class);
XmlTag parentXmlTag = PsiTreeUtil.getParentOfType(currentXmlTag, XmlTag.class);
if(parentXmlTag == null) {
return;
}
String name = parentXmlTag.getName();
if(name.equals("service")) {
// service/argument[id]
XmlAttribute classAttribute = parentXmlTag.getAttribute("class");
if(classAttribute != null) {
String serviceDefName = classAttribute.getValue();
if(serviceDefName != null) {
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(psiElement.getProject(), serviceDefName);
// check type hint on constructor
if(phpClass != null) {
Method constructor = phpClass.getConstructor();
if(constructor != null) {
String serviceName = ((XmlAttributeValue) psiElement).getValue();
attachMethodInstances(psiElement, serviceName, constructor, getArgumentIndex(currentXmlTag), holder);
}
}
}
}
} else if (name.equals("call")) {
// service/call/argument[id]
XmlAttribute methodAttribute = parentXmlTag.getAttribute("method");
if(methodAttribute != null) {
String methodName = methodAttribute.getValue();
XmlTag serviceTag = parentXmlTag.getParentTag();
// get service class
if(serviceTag != null && "service".equals(serviceTag.getName())) {
XmlAttribute classAttribute = serviceTag.getAttribute("class");
if(classAttribute != null) {
String serviceDefName = classAttribute.getValue();
if(serviceDefName != null) {
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(psiElement.getProject(), serviceDefName);
// finally check method type hint
if(phpClass != null) {
Method method = phpClass.findMethodByName(methodName);
if(method != null) {
String serviceName = ((XmlAttributeValue) psiElement).getValue();
attachMethodInstances(psiElement, serviceName, method, getArgumentIndex(currentXmlTag), holder);
}
}
}
}
}
}
}
}
private void attachMethodInstances(PsiElement target, String serviceName, Method method, int parameterIndex, @NotNull AnnotationHolder holder) {
Parameter[] constructorParameter = method.getParameters();
if(parameterIndex >= constructorParameter.length) {
return;
}
String className = constructorParameter[parameterIndex].getDeclaredType().toString();
PhpClass expectedClass = PhpElementsUtil.getClassInterface(method.getProject(), className);
if(expectedClass == null) {
return;
}
PhpClass serviceParameterClass = ServiceUtil.getResolvedClassDefinition(method.getProject(), serviceName);
if(serviceParameterClass != null && !PhpElementsUtil.isInstanceOf(serviceParameterClass, expectedClass)) {
holder.createWarningAnnotation(target, "Expect instance of: " + expectedClass.getPresentableFQN())
.registerFix(new MySuggestionIntentionAction(expectedClass, target));
}
}
/**
* Returns current index of parent tag
* <foo>
* <argument/>
* <arg<caret>ument/>
* </foo>
*/
public static int getArgumentIndex(@NotNull XmlTag xmlTag) {
PsiElement psiElement = xmlTag;
int index = 0;
while (psiElement != null) {
psiElement = psiElement.getPrevSibling();
if(psiElement instanceof XmlTag && "argument".equalsIgnoreCase(((XmlTag) psiElement).getName())) {
index++;
}
}
return index;
}
private static class MySuggestionIntentionAction extends PsiElementBaseIntentionAction {
private final PhpClass expectedClass;
private final PsiElement target;
public MySuggestionIntentionAction(@NotNull PhpClass expectedClass, @NotNull PsiElement target) {
this.expectedClass = expectedClass;
this.target = target;
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return "Symfony";
}
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement psiElement) throws IncorrectOperationException {
Collection<ContainerService> suggestions = ServiceUtil.getServiceSuggestionForPhpClass(expectedClass, ContainerCollectionResolver.getServices(project));
if(suggestions.size() == 0) {
HintManager.getInstance().showErrorHint(editor, "No suggestion found");
return;
}
XmlTag xmlTag = PsiTreeUtil.getParentOfType(target, XmlTag.class);
if(xmlTag == null) {
return;
}
ServiceSuggestDialog.create(
editor,
ContainerUtil.map(suggestions, ContainerService::getName),
new XmlServiceSuggestIntention.MyInsertCallback(xmlTag)
);
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement psiElement) {
return true;
}
@NotNull
@Override
public String getText() {
return "Symfony: Suggest Service";
}
}
}