package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;
import com.intellij.patterns.PlatformPatterns;
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.containers.ContainerUtil;
import com.intellij.util.containers.HashSet;
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.parser.PhpDocElementTypes;
import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
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.PhpPsiElement;
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.routing.RouteHelper;
import fr.adrienbrault.idea.symfony2plugin.stubs.dict.StubIndexedRoute;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.ObjectStreamDataExternalizer;
import fr.adrienbrault.idea.symfony2plugin.util.AnnotationBackportUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import gnu.trove.THashMap;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class AnnotationRoutesStubIndex extends FileBasedIndexExtension<String, StubIndexedRoute> {
public static final ID<String, StubIndexedRoute> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.annotation_routes");
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
private static ObjectStreamDataExternalizer<StubIndexedRoute> EXTERNALIZER = new ObjectStreamDataExternalizer<>();
@NotNull
@Override
public ID<String, StubIndexedRoute> getName() {
return KEY;
}
@NotNull
@Override
public DataIndexer<String, StubIndexedRoute, FileContent> getIndexer() {
return inputData -> {
final Map<String, StubIndexedRoute> map = new THashMap<>();
PsiFile psiFile = inputData.getPsiFile();
if(!Symfony2ProjectComponent.isEnabledForIndex(psiFile.getProject())) {
return map;
}
if(!(inputData.getPsiFile() instanceof PhpFile)) {
return map;
}
if(!RoutesStubIndex.isValidForIndex(inputData, psiFile)) {
return map;
}
psiFile.accept(new MyPsiRecursiveElementWalkingVisitor(map));
return map;
};
}
@NotNull
@Override
public KeyDescriptor<String> getKeyDescriptor() {
return this.myKeyDescriptor;
}
@NotNull
@Override
public DataExternalizer<StubIndexedRoute> 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 10;
}
@Nullable
public static String getClassNameReference(PhpDocTag phpDocTag) {
return getClassNameReference(phpDocTag, AnnotationBackportUtil.getUseImportMap(phpDocTag));
}
@Nullable
public static String getClassNameReference(PhpDocTag phpDocTag, Map<String, String> useImports) {
if(useImports.size() == 0) {
return null;
}
String annotationName = phpDocTag.getName();
if(StringUtils.isBlank(annotationName)) {
return null;
}
if(annotationName.startsWith("@")) {
annotationName = annotationName.substring(1);
}
String className = annotationName;
String subNamespaceName = "";
if(className.contains("\\")) {
className = className.substring(0, className.indexOf("\\"));
subNamespaceName = annotationName.substring(className.length());
}
if(!useImports.containsKey(className)) {
return null;
}
// normalize name
String annotationFqnName = useImports.get(className) + subNamespaceName;
if(!annotationFqnName.startsWith("\\")) {
annotationFqnName = "\\" + annotationFqnName;
}
return annotationFqnName;
}
private static class MyPsiRecursiveElementWalkingVisitor extends PsiRecursiveElementWalkingVisitor {
private final Map<String, StubIndexedRoute> map;
private Map<String, String> fileImports;
public MyPsiRecursiveElementWalkingVisitor(Map<String, StubIndexedRoute> map) {
this.map = map;
}
@Override
public void visitElement(PsiElement element) {
if ((element instanceof PhpDocTag)) {
visitPhpDocTag((PhpDocTag) element);
}
super.visitElement(element);
}
public void visitPhpDocTag(PhpDocTag phpDocTag) {
// "@var" and user non related tags dont need an action
if(AnnotationBackportUtil.NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
return;
}
// init file imports
if(this.fileImports == null) {
this.fileImports = AnnotationBackportUtil.getUseImportMap(phpDocTag);
}
if(this.fileImports.size() == 0) {
return;
}
String annotationFqnName = AnnotationRoutesStubIndex.getClassNameReference(phpDocTag, this.fileImports);
if(annotationFqnName == null || !RouteHelper.isRouteClassAnnotation(annotationFqnName)) {
return;
}
PsiElement phpDocAttributeList = PsiElementUtils.getChildrenOfType(phpDocTag, PlatformPatterns.psiElement(PhpDocElementTypes.phpDocAttributeList));
if(!(phpDocAttributeList instanceof PhpPsiElement)) {
return;
}
String routeName = AnnotationBackportUtil.getAnnotationRouteName(phpDocAttributeList.getText());
if(routeName == null) {
routeName = AnnotationBackportUtil.getRouteByMethod(phpDocTag);
}
if(routeName != null && StringUtils.isNotBlank(routeName)) {
StubIndexedRoute route = new StubIndexedRoute(routeName);
String path = "";
// get class scope pattern
String classPath = getClassRoutePattern(phpDocTag);
if(classPath != null) {
path += classPath;
}
// extract method path
PhpPsiElement firstPsiChild = ((PhpPsiElement) phpDocAttributeList).getFirstPsiChild();
if(firstPsiChild instanceof StringLiteralExpression) {
String contents = ((StringLiteralExpression) firstPsiChild).getContents();
if(StringUtils.isNotBlank(contents)) {
path += contents;
}
}
if (path.length() > 0) {
route.setPath(path);
}
route.setController(getController(phpDocTag));
// @Method(...)
extractMethods(phpDocTag, route);
map.put(routeName, route);
}
}
private void extractMethods(@NotNull PhpDocTag phpDocTag, @NotNull StubIndexedRoute route) {
PsiElement phpDoc = phpDocTag.getParent();
if(!(phpDoc instanceof PhpDocComment)) {
return;
}
PsiElement methodTag = ContainerUtil.find(phpDoc.getChildren(), psiElement ->
psiElement instanceof PhpDocTag &&
"\\Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Method".equals(
AnnotationRoutesStubIndex.getClassNameReference((PhpDocTag) psiElement, fileImports)
)
);
if(!(methodTag instanceof PhpDocTag)) {
return;
}
PhpPsiElement attrList = ((PhpDocTag) methodTag).getFirstPsiChild();
if(attrList == null || attrList.getNode().getElementType() != PhpDocElementTypes.phpDocAttributeList) {
return;
}
String content = attrList.getText();
// ({"POST", "GET"}), ("POST")
Matcher matcher = Pattern.compile("\"([\\w]{3,7})\"", Pattern.DOTALL).matcher(content);
Collection<String> methods = new HashSet<>();
while (matcher.find()) {
methods.add(matcher.group(1).toLowerCase());
}
if(methods.size() > 0) {
route.setMethods(methods);
}
}
/**
* FooController::fooAction
*/
@Nullable
private String getController(@NotNull PhpDocTag phpDocTag) {
Method method = AnnotationBackportUtil.getMethodScope(phpDocTag);
if(method == null) {
return null;
}
PhpClass containingClass = method.getContainingClass();
if(containingClass == null) {
return null;
}
return String.format("%s::%s",
StringUtils.stripStart(containingClass.getFQN(), "\\"),
method.getName()
);
}
@Nullable
private String getClassRoutePattern(@NotNull PhpDocTag phpDocTag) {
PhpClass phpClass = PsiTreeUtil.getParentOfType(phpDocTag, PhpClass.class);
if(phpClass == null) {
return null;
}
PhpDocComment docComment = phpClass.getDocComment();
for (PhpDocTag docTag : PsiTreeUtil.getChildrenOfTypeAsList(docComment, PhpDocTag.class)) {
String classNameReference = AnnotationRoutesStubIndex.getClassNameReference(docTag, this.fileImports);
if(classNameReference == null) {
continue;
}
if(!RouteHelper.isRouteClassAnnotation(classNameReference)) {
continue;
}
PsiElement docAttr = PsiElementUtils.getChildrenOfType(docTag, PlatformPatterns.psiElement(PhpDocElementTypes.phpDocAttributeList));
if(!(docAttr instanceof PhpPsiElement)) {
continue;
}
PhpPsiElement firstPsiChild = ((PhpPsiElement) docAttr).getFirstPsiChild();
if(!(firstPsiChild instanceof StringLiteralExpression)) {
continue;
}
String contents = ((StringLiteralExpression) firstPsiChild).getContents();
if(StringUtils.isNotBlank(contents)) {
return contents;
}
}
return null;
}
}
}