package fr.adrienbrault.idea.symfony2plugin.config.yaml.inspection;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.StandardPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiRecursiveElementWalkingVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlFile;
import com.intellij.util.Consumer;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.Method;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.php.lang.psi.elements.PhpReturn;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.codeInspection.quickfix.CreateMethodQuickFix;
import fr.adrienbrault.idea.symfony2plugin.config.EventDispatcherSubscriberUtil;
import fr.adrienbrault.idea.symfony2plugin.config.xml.XmlHelper;
import fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlElementPatternHelper;
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
import fr.adrienbrault.idea.symfony2plugin.util.AnnotationBackportUtil;
import fr.adrienbrault.idea.symfony2plugin.util.EventSubscriberUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.YAMLTokenTypes;
import org.jetbrains.yaml.psi.*;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class EventMethodCallInspection extends LocalInspectionTool {
@NotNull
@Override
public PsiElementVisitor buildVisitor(final @NotNull ProblemsHolder holder, boolean isOnTheFly) {
if(!Symfony2ProjectComponent.isEnabled(holder.getProject())) {
return super.buildVisitor(holder, isOnTheFly);
}
return new PsiElementVisitor() {
@Override
public void visitFile(PsiFile psiFile) {
if(psiFile instanceof XmlFile) {
visitXmlFile(psiFile, holder, new ContainerCollectionResolver.LazyServiceCollector(holder.getProject()));
} else if(psiFile instanceof YAMLFile) {
visitYamlFile(psiFile, holder, new ContainerCollectionResolver.LazyServiceCollector(holder.getProject()));
} else if(psiFile instanceof PhpFile) {
visitPhpFile((PhpFile) psiFile, holder);
}
}
};
}
private void visitPhpFile(PhpFile psiFile, final ProblemsHolder holder) {
psiFile.acceptChildren(new PhpSubscriberRecursiveElementWalkingVisitor(holder));
}
private void visitYamlFile(PsiFile psiFile, final ProblemsHolder holder, @NotNull final ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) {
psiFile.acceptChildren(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
annotateCallMethod(element, holder, lazyServiceCollector);
super.visitElement(element);
}
});
}
private void visitXmlFile(@NotNull PsiFile psiFile, @NotNull final ProblemsHolder holder, @NotNull final ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) {
psiFile.acceptChildren(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
if(XmlHelper.getTagAttributePattern("tag", "method").inside(XmlHelper.getInsideTagPattern("services")).inFile(XmlHelper.getXmlFilePattern()).accepts(element) ||
XmlHelper.getTagAttributePattern("call", "method").inside(XmlHelper.getInsideTagPattern("services")).inFile(XmlHelper.getXmlFilePattern()).accepts(element)
)
{
// attach to text child only
PsiElement[] psiElements = element.getChildren();
if(psiElements.length < 2) {
return;
}
String serviceClassValue = XmlHelper.getServiceDefinitionClass(element);
if(serviceClassValue != null && StringUtils.isNotBlank(serviceClassValue)) {
registerMethodProblem(psiElements[1], holder, serviceClassValue, lazyServiceCollector);
}
}
super.visitElement(element);
}
});
}
private void visitYamlMethodTagKey(@NotNull final PsiElement psiElement, @NotNull ProblemsHolder holder, ContainerCollectionResolver.LazyServiceCollector collector) {
String methodName = PsiElementUtils.trimQuote(psiElement.getText());
if(StringUtils.isBlank(methodName)) {
return;
}
String classValue = YamlHelper.getServiceDefinitionClass(psiElement);
if(classValue == null) {
return;
}
registerMethodProblem(psiElement, holder, classValue, collector);
}
private void annotateCallMethod(@NotNull final PsiElement psiElement, @NotNull ProblemsHolder holder, ContainerCollectionResolver.LazyServiceCollector collector) {
if(StandardPatterns.and(
YamlElementPatternHelper.getInsideKeyValue("tags"),
YamlElementPatternHelper.getSingleLineScalarKey("method")
).accepts(psiElement)) {
visitYamlMethodTagKey(psiElement, holder, collector);
}
if((PlatformPatterns.psiElement(YAMLTokenTypes.TEXT).accepts(psiElement)
|| PlatformPatterns.psiElement(YAMLTokenTypes.SCALAR_DSTRING).accepts(psiElement)))
{
visitYamlMethod(psiElement, holder, collector);
}
}
private void visitYamlMethod(PsiElement psiElement, ProblemsHolder holder, ContainerCollectionResolver.LazyServiceCollector collector) {
if(YamlElementPatternHelper.getInsideKeyValue("calls").accepts(psiElement)) {
PsiElement parent = psiElement.getParent();
if ((parent instanceof YAMLScalar)) {
YamlHelper.visitServiceCall((YAMLScalar) parent, s ->
registerMethodProblem(psiElement, holder, YamlHelper.trimSpecialSyntaxServiceName(s), collector)
);
}
}
}
private void registerMethodProblem(final @NotNull PsiElement psiElement, @NotNull ProblemsHolder holder, @NotNull String classKeyValue, ContainerCollectionResolver.LazyServiceCollector collector) {
registerMethodProblem(psiElement, holder, ServiceUtil.getResolvedClassDefinition(psiElement.getProject(), classKeyValue, collector));
}
private void registerMethodProblem(final @NotNull PsiElement psiElement, @NotNull ProblemsHolder holder, @Nullable PhpClass phpClass) {
if(phpClass == null) {
return;
}
final String methodName = PsiElementUtils.trimQuote(psiElement.getText());
if(phpClass.findMethodByName(methodName) != null) {
return;
}
holder.registerProblem(
psiElement,
"Missing Method",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
new CreateMethodQuickFix(phpClass, methodName, new MyCreateMethodQuickFix())
);
}
private static class MyCreateMethodQuickFix implements CreateMethodQuickFix.InsertStringInterface {
@NotNull
@Override
public StringBuilder getStringBuilder(@NotNull ProblemDescriptor problemDescriptor, @NotNull PhpClass phpClass, @NotNull String functionName) {
String taggedEventMethodParameter = getEventTypeHint(problemDescriptor, phpClass);
String parameter = "";
if(taggedEventMethodParameter != null) {
parameter = taggedEventMethodParameter + " $event";
}
return new StringBuilder()
.append("public function ")
.append(functionName)
.append("(")
.append(parameter)
.append(")\n {\n}\n\n");
}
@Nullable
private String getEventTypeHint(@NotNull ProblemDescriptor problemDescriptor, @NotNull PhpClass phpClass) {
String eventName = EventDispatcherSubscriberUtil.getEventNameFromScope(problemDescriptor.getPsiElement());
if (eventName == null) {
return null;
}
String taggedEventMethodParameter = EventSubscriberUtil.getTaggedEventMethodParameter(problemDescriptor.getPsiElement().getProject(), eventName);
if (taggedEventMethodParameter == null) {
return null;
}
String qualifiedName = AnnotationBackportUtil.getQualifiedName(phpClass, taggedEventMethodParameter);
if (qualifiedName != null && !qualifiedName.equals(StringUtils.stripStart(taggedEventMethodParameter, "\\"))) {
// class already imported
return qualifiedName;
}
return PhpElementsUtil.insertUseIfNecessary(phpClass, taggedEventMethodParameter);
}
}
private String getServiceName(PsiElement psiElement) {
return YamlHelper.trimSpecialSyntaxServiceName(PsiElementUtils.getText(psiElement));
}
/**
* getSubscribedEvents method quick fix check
*
* return array(
* ConsoleEvents::COMMAND => array('onCommanda', 255),
* ConsoleEvents::TERMINATE => array('onTerminate', -255),
* );
*
*/
private class PhpSubscriberRecursiveElementWalkingVisitor extends PsiRecursiveElementWalkingVisitor {
private final ProblemsHolder holder;
public PhpSubscriberRecursiveElementWalkingVisitor(ProblemsHolder holder) {
this.holder = holder;
}
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if(!(element instanceof StringLiteralExpression)) {
return;
}
PsiElement arrayValue = element.getParent();
if(arrayValue != null && arrayValue.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
PhpReturn phpReturn = PsiTreeUtil.getParentOfType(arrayValue, PhpReturn.class);
if(phpReturn != null) {
Method method = PsiTreeUtil.getParentOfType(arrayValue, Method.class);
if(method != null) {
String name = method.getName();
if("getSubscribedEvents".equals(name)) {
PhpClass containingClass = method.getContainingClass();
if(containingClass != null && PhpElementsUtil.isInstanceOf(containingClass, "\\Symfony\\Component\\EventDispatcher\\EventSubscriberInterface")) {
String contents = ((StringLiteralExpression) element).getContents();
if(StringUtils.isNotBlank(contents) && containingClass.findMethodByName(contents) == null) {
registerMethodProblem(element, holder, containingClass);
}
}
}
}
}
}
}
}
}