package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiRecursiveElementVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
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.PhpFileType;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.dic.container.dict.ContainerBuilderCall;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.ObjectStreamDataExternalizer;
import gnu.trove.THashMap;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Set;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*
* @see fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ContainerBuilderStubIndex
*/
public class ContainerBuilderStubIndex extends FileBasedIndexExtension<String, ContainerBuilderCall> {
public static final ID<String, ContainerBuilderCall> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.container_builder");
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
private final static ObjectStreamDataExternalizer<ContainerBuilderCall> EXTERNALIZER = new ObjectStreamDataExternalizer<>();
private static int MAX_FILE_BYTE_SIZE = 2621440;
private static final Set<String> SET = new HashSet<String>() {{
add("Symfony\\Component\\DependencyInjection\\Container");
add("Symfony\\Component\\DependencyInjection\\ContainerBuilder");
add("Symfony\\Component\\DependencyInjection\\TaggedContainerInterface");
}};
private static final Set<String> METHODS = new HashSet<String>() {{
add("findTaggedServiceIds");
add("setDefinition");
add("setParameter");
add("setAlias");
add("register");
}};
@NotNull
@Override
public DataIndexer<String, ContainerBuilderCall, FileContent> getIndexer() {
return inputData -> {
Map<String, ContainerBuilderCall> map = new THashMap<>();
PsiFile psiFile = inputData.getPsiFile();
if(!(psiFile instanceof PhpFile) ||
!Symfony2ProjectComponent.isEnabledForIndex(psiFile.getProject()) ||
!isValidForIndex(inputData, psiFile)
){
return map;
}
psiFile.accept(new MyPsiRecursiveElementWalkingVisitor(map));
return map;
};
}
@NotNull
@Override
public ID<String, ContainerBuilderCall> getName() {
return KEY;
}
@NotNull
@Override
public KeyDescriptor<String> getKeyDescriptor() {
return this.myKeyDescriptor;
}
@NotNull
public DataExternalizer<ContainerBuilderCall> getValueExternalizer() {
return EXTERNALIZER;
}
@NotNull
@Override
public FileBasedIndex.InputFilter getInputFilter() {
return virtualFile -> virtualFile.getFileType() == PhpFileType.INSTANCE;
}
@Override
public boolean dependsOnFileContent() {
return true;
}
@Override
public int getVersion() {
return 2;
}
private static boolean isValidForIndex(FileContent inputData, PsiFile psiFile) {
String fileName = psiFile.getName();
if(fileName.startsWith(".") || fileName.endsWith("Test")) {
return false;
}
// is Test file in path name
String relativePath = VfsUtil.getRelativePath(inputData.getFile(), psiFile.getProject().getBaseDir(), '/');
if(relativePath != null && (relativePath.contains("/Test/") || relativePath.contains("/Tests/") || relativePath.contains("/Fixture/") || relativePath.contains("/Fixtures/"))) {
return false;
}
// dont index files larger then files; use 5 MB here
if(inputData.getFile().getLength() > MAX_FILE_BYTE_SIZE) {
return false;
}
return true;
}
private class MyPsiRecursiveElementWalkingVisitor extends PsiRecursiveElementVisitor {
private final Map<String, ContainerBuilderCall> map;
MyPsiRecursiveElementWalkingVisitor(@NotNull Map<String, ContainerBuilderCall> map) {
this.map = map;
}
@Override
public void visitElement(PsiElement element) {
if(!(element instanceof Parameter)) {
super.visitElement(element);
return;
}
ClassReference classReference = ObjectUtils.tryCast(element.getFirstChild(), ClassReference.class);
if(classReference == null) {
return;
}
String fqn = StringUtils.stripStart(classReference.getFQN(), "\\");
if(!SET.contains(fqn)) {
return;
}
Parameter parentOfType = PsiTreeUtil.getParentOfType(classReference, Parameter.class);
if(parentOfType == null) {
return;
}
final String name = parentOfType.getName();
Method method = PsiTreeUtil.getParentOfType(classReference, Method.class);
if(method == null) {
return;
}
method.accept(new MyMethodVariableVisitor(name, map));
super.visitElement(element);
}
}
private class MyMethodVariableVisitor extends PsiRecursiveElementVisitor {
@NotNull
private final String name;
@NotNull
private final Map<String, ContainerBuilderCall> result;
MyMethodVariableVisitor(@NotNull String name, @NotNull Map<String, ContainerBuilderCall> result) {
this.name = name;
this.result = result;
}
@Override
public void visitElement(PsiElement element) {
if(!(element instanceof Variable) || !name.equals(((Variable) element).getName())) {
super.visitElement(element);
return;
}
MethodReference methodReference = ObjectUtils.tryCast(element.getParent(), MethodReference.class);
if(methodReference == null || !METHODS.contains(methodReference.getName())) {
super.visitElement(element);
return;
}
String value = Symfony2InterfacesUtil.getFirstArgumentStringValue(methodReference);
if(value == null) {
super.visitElement(element);
return;
}
String methodName = methodReference.getName();
if(!result.containsKey(methodName)) {
result.put(methodName, new ContainerBuilderCall());
}
result.get(methodName).addParameter(value);
super.visitElement(element);
}
}
}