package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiRecursiveElementWalkingVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.indexing.*;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.KeyDescriptor;
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.Function;
import com.jetbrains.php.lang.psi.elements.Method;
import com.jetbrains.php.lang.psi.elements.MethodReference;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
import com.jetbrains.php.lang.psi.stubs.indexes.PhpConstantNameIndex;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.TwigHelper;
import fr.adrienbrault.idea.symfony2plugin.stubs.dict.TemplateUsage;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.ObjectStreamDataExternalizer;
import fr.adrienbrault.idea.symfony2plugin.util.AnnotationBackportUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class PhpTwigTemplateUsageStubIndex extends FileBasedIndexExtension<String, TemplateUsage> {
public static final ID<String, TemplateUsage> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.twig_php_usage");
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
private static int MAX_FILE_BYTE_SIZE = 2097152;
private static ObjectStreamDataExternalizer<TemplateUsage> EXTERNALIZER = new ObjectStreamDataExternalizer<>();
public static Set<String> RENDER_METHODS = new HashSet<String>() {{
add("render");
add("renderView");
add("renderResponse");
}};
@NotNull
@Override
public ID<String, TemplateUsage> getName() {
return KEY;
}
@NotNull
@Override
public DataIndexer<String, TemplateUsage, FileContent> getIndexer() {
return new DataIndexer<String, TemplateUsage, FileContent>() {
@NotNull
@Override
public Map<String, TemplateUsage> map(@NotNull FileContent inputData) {
PsiFile psiFile = inputData.getPsiFile();
if(!Symfony2ProjectComponent.isEnabledForIndex(psiFile.getProject())) {
return Collections.emptyMap();
}
if(!(inputData.getPsiFile() instanceof PhpFile) && isValidForIndex(inputData)) {
return Collections.emptyMap();
}
Map<String, Set<String>> items = new HashMap<>();
psiFile.accept(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
if(element instanceof MethodReference) {
visitMethodReference((MethodReference) element);
} else if(element instanceof PhpDocTag) {
visitPhpDocTag((PhpDocTag) element);
}
super.visitElement(element);
}
private void visitMethodReference(@NotNull MethodReference methodReference) {
String methodName = methodReference.getName();
if(!RENDER_METHODS.contains(methodName)) {
return;
}
PsiElement[] parameters = methodReference.getParameters();
if(parameters.length == 0 || !(parameters[0] instanceof StringLiteralExpression)) {
return;
}
String contents = ((StringLiteralExpression) parameters[0]).getContents();
if(StringUtils.isBlank(contents) || !contents.endsWith(".html.twig")) {
return;
}
Function parentOfType = PsiTreeUtil.getParentOfType(methodReference, Function.class);
if(parentOfType == null) {
return;
}
addTemplateWithScope(contents, StringUtils.stripStart(parentOfType.getFQN(), "\\"));
}
/**
* "@Template("foobar.html.twig")"
* "@Template(template="foobar.html.twig")"
*/
private void visitPhpDocTag(@NotNull PhpDocTag phpDocTag) {
// "@var" and user non related tags dont need an action
if(AnnotationBackportUtil.NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
return;
}
// init scope imports
Map<String, String> fileImports = AnnotationBackportUtil.getUseImportMap(phpDocTag);
if(fileImports.size() == 0) {
return;
}
String annotationFqnName = AnnotationRoutesStubIndex.getClassNameReference(phpDocTag, fileImports);
if(!"Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Template".equals(StringUtils.stripStart(annotationFqnName, "\\"))) {
return;
}
String template = AnnotationBackportUtil.getDefaultOrPropertyContents(phpDocTag, "template");
if(template != null && template.endsWith(".html.twig")) {
Method methodScope = AnnotationBackportUtil.getMethodScope(phpDocTag);
if(methodScope != null) {
addTemplateWithScope(template, StringUtils.stripStart(methodScope.getFQN(), "\\"));
}
}
}
private void addTemplateWithScope(@NotNull String contents, @NotNull String fqn) {
String s = TwigHelper.normalizeTemplateName(contents);
if(!items.containsKey(s)) {
items.put(s, new HashSet<>());
}
items.get(s).add(fqn);
}
});
Map<String, TemplateUsage> map = new HashMap<>();
items.entrySet().forEach(entry ->
map.put(entry.getKey(), new TemplateUsage(entry.getKey(), entry.getValue()))
);
return map;
}
};
}
@NotNull
@Override
public KeyDescriptor<String> getKeyDescriptor() {
return this.myKeyDescriptor;
}
@NotNull
@Override
public DataExternalizer<TemplateUsage> getValueExternalizer() {
return EXTERNALIZER;
}
@NotNull
@Override
public FileBasedIndex.InputFilter getInputFilter() {
return PhpConstantNameIndex.PHP_INPUT_FILTER;
}
@Override
public boolean dependsOnFileContent() {
return true;
}
@Override
public int getVersion() {
return 3;
}
public static boolean isValidForIndex(FileContent inputData) {
return inputData.getFile().getLength() < MAX_FILE_BYTE_SIZE;
}
}