package fr.adrienbrault.idea.symfony2plugin.doctrine.querybuilder.processor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class QueryBuilderChainProcessor {
private static final String DOCTRINE_ORM_QUERY_BUILDER = "\\Doctrine\\ORM\\QueryBuilder";
final private MethodReference startMethodRef;
private List<MethodReference> queryBuilderMethodReferences;
private List<MethodReference> queryBuilderFactoryMethods;
public QueryBuilderChainProcessor(MethodReference psiElement) {
this.startMethodRef = psiElement;
}
public void collectMethods() {
// get chaining methods references after current one and reverse it; to get some right ordering
List<MethodReference> methodReferences = getMethodReferencesAfter(startMethodRef);
Collections.reverse(methodReferences);
List<MethodReference> factoryReferences = new ArrayList<>();
processUpChainingMethods(startMethodRef, methodReferences, factoryReferences, true);
this.queryBuilderMethodReferences = methodReferences;
this.queryBuilderFactoryMethods = factoryReferences;
}
public List<MethodReference> getQueryBuilderMethodReferences() {
return queryBuilderMethodReferences;
}
public List<MethodReference> getQueryBuilderFactoryMethods() {
return queryBuilderFactoryMethods;
}
/**
* We are inside addSelect and want to get orderBy and orderBy
*
* $qb->addSelect('<here>')->orderBy()->orderBy()
*
* @param methodReference current method
*/
@NotNull
public List<MethodReference> getMethodReferencesAfter(@NotNull MethodReference methodReference) {
List<MethodReference> methodReferences = new ArrayList<>();
PsiElement parent = methodReference;
while (parent instanceof MethodReference) {
PsiElement method = ((MethodReference) parent).resolve();
if(method instanceof Method && isQueryBuilderInstance((Method) method) == InstanceType.DIRECT) {
methodReferences.add((MethodReference) parent);
}
parent = parent.getParent();
}
return methodReferences;
}
public void processNextMethodReference(Method method, List<MethodReference> medRefCollection, List<MethodReference> factoryReferences) {
for(PhpReturn phpReturn: PsiTreeUtil.collectElementsOfType(method, PhpReturn.class)) {
PsiElement child = phpReturn.getFirstPsiChild();
processUpChainingMethods(child, medRefCollection, factoryReferences, true);
}
}
private void processVariableScope(List<MethodReference> medRefCollection, List<MethodReference> factoryReferences, Variable child) {
for(Variable varRef: PhpElementsUtil.getVariableReferencesInScope(child)) {
// we need to handle variables and their declaration in different ways
if(varRef.isDeclaration()) {
// we are inside var declaration, so our expression is already "select", no further action
// $var = $qb->addSelect()->select();
PsiElement assignExpress = varRef.getParent();
if(assignExpress instanceof AssignmentExpression) {
PhpPsiElement metRef = ((AssignmentExpression) assignExpress).getValue();
if(metRef instanceof MethodReference) {
processUpChainingMethods(metRef, medRefCollection, factoryReferences, false);
}
}
} else {
// we get "$qb" first but to provide right chaining we need deepest method reference "select",
// phpstorm lexer provide chaining methods in reverse order
// we need to walk psi tree up until last method references call
// $qb->addSelect()->select()
MethodReference methodReference = getLastParentOfType(varRef, MethodReference.class);
if(methodReference != null) {
processUpChainingMethods(methodReference, medRefCollection, factoryReferences, false);
}
}
}
}
public void processUpChainingMethods(PsiElement psiElement, List<MethodReference> medRefCollection, List<MethodReference> factoryReferences, boolean resolveVar) {
PsiElement child = psiElement;
while (child instanceof MethodReference) {
// stop on invalid item like factory method eg createQueryBuilder
if (!nextMethodScope(medRefCollection, factoryReferences, (MethodReference) child)) {
return;
}
child = ((MethodReference) child).getFirstPsiChild();
}
// $qb->addSelect();
// $this->foo() = invalid
if(resolveVar && child instanceof Variable && !"this".equals(((Variable) child).getName())) {
processVariableScope(medRefCollection, factoryReferences, (Variable) child);
}
}
private boolean nextMethodScope(List<MethodReference> medRefCollection, List<MethodReference> factoryReferences, MethodReference child) {
// get original method
PsiElement method = child.resolve();
if(!(method instanceof Method)) {
return false;
}
InstanceType queryBuilderInstance = isQueryBuilderInstance((Method) method);
switch (queryBuilderInstance) {
case DIRECT:
// we are inside QueryBuilder class eg. addSelect
medRefCollection.add(child);
return true;
case RESOLVE:
// we found a method returning QueryBuilder
// stop on direct querybuilder factory method
if ("createQueryBuilder".equals(((Method) method).getName())) {
factoryReferences.add(child);
return false;
}
// goto (resolve) method and collect its QueryBuilder method
processNextMethodReference((Method) method, medRefCollection, factoryReferences);
return true;
default:
factoryReferences.add(child);
return false;
}
}
@NotNull
private static InstanceType isQueryBuilderInstance(@NotNull Method method) {
PhpClass containingClass = method.getContainingClass();
if(containingClass != null && PhpElementsUtil.isInstanceOf(containingClass, DOCTRINE_ORM_QUERY_BUILDER)) {
return InstanceType.DIRECT;
}
for(PhpClass phpClass: PhpElementsUtil.getClassFromPhpTypeSet(method.getProject(), method.getType().getTypes())) {
if(PhpElementsUtil.isEqualClassName(phpClass, DOCTRINE_ORM_QUERY_BUILDER)) {
return InstanceType.RESOLVE;
}
}
return InstanceType.NONE;
}
public enum InstanceType {
NONE, DIRECT, RESOLVE
}
@Nullable
private static <T extends PsiElement> T getLastParentOfType(@Nullable PsiElement element, @NotNull Class<T> aClass) {
if (element == null) return null;
PsiElement last = element.getParent();
if(!aClass.isInstance(last)) {
return null;
}
while (aClass.isInstance(last)) {
if (element instanceof PsiFile) return null;
element = element.getParent();
if(!aClass.isInstance(element)) {
//noinspection unchecked
return (T)last;
}
last = element;
}
//noinspection unchecked
return null;
}
}