package fr.adrienbrault.idea.symfony2plugin.templating.util;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil;
import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class PhpMethodVariableResolveUtil {
/**
* search for twig template variable on common use cases
*
* $this->render('foobar.html.twig', $foobar)
* $this->render('foobar.html.twig', ['foobar' => $var]))
* $this->render('foobar.html.twig', array_merge($foobar, ['foobar' => $var]))
* $this->render('foobar.html.twig', array_merge_recursive($foobar, ['foobar' => $var]))
* $this->render('foobar.html.twig', array_push($foobar, ['foobar' => $var]))
* $this->render('foobar.html.twig', array_replace($foobar, ['foobar' => $var]))
* $this->render('foobar.html.twig', $foobar + ['foobar' => $var])
* $this->render('foobar.html.twig', $foobar += ['foobar' => $var])
*/
public static Map<String, PsiVariable> collectMethodVariables(@NotNull Function method) {
Map<String, PsiVariable> collectedTypes = new HashMap<>();
for(PsiElement var: collectPossibleTemplateArrays(method)) {
if(var instanceof ArrayCreationExpression) {
// "return array(...)" we dont need any parsing
collectedTypes.putAll(getTypesOnArrayHash((ArrayCreationExpression) var));
} else if(var instanceof Variable) {
// we need variable declaration line so resolve it and search for references which attach other values to array
// find definition and search for references on it
PsiElement resolvedVariable = ((Variable) var).resolve();
if(resolvedVariable instanceof Variable) {
collectedTypes.putAll(collectOnVariableReferences(method, (Variable) resolvedVariable));
}
} else if(var instanceof FunctionReference && "array_merge".equalsIgnoreCase(((FunctionReference) var).getName())) {
// array_merge($var, ['foobar' => $var]);
String name = ((FunctionReference) var).getName();
if("array_merge".equalsIgnoreCase(name) || "array_merge_recursive".equalsIgnoreCase(name) || "array_push".equalsIgnoreCase(name) || "array_replace".equalsIgnoreCase(name)) {
for (PsiElement psiElement : ((FunctionReference) var).getParameters()) {
collectVariablesForPsiElement(method, collectedTypes, psiElement);
}
}
} else if(var instanceof BinaryExpression && var.getNode().getElementType() == PhpElementTypes.ADDITIVE_EXPRESSION) {
// $var + ['foobar' => $foobar]
PsiElement leftOperand = ((BinaryExpression) var).getLeftOperand();
if(leftOperand != null) {
collectVariablesForPsiElement(method, collectedTypes, leftOperand);
}
PsiElement rightOperand = ((BinaryExpression) var).getRightOperand();
if(rightOperand != null) {
collectVariablesForPsiElement(method, collectedTypes, rightOperand);
}
} else if(var instanceof SelfAssignmentExpression) {
// $var += ['foobar' => $foobar]
PhpPsiElement variable = ((SelfAssignmentExpression) var).getVariable();
if(variable != null) {
collectVariablesForPsiElement(method, collectedTypes, variable);
}
PhpPsiElement value = ((SelfAssignmentExpression) var).getValue();
if(value != null) {
collectVariablesForPsiElement(method, collectedTypes, value);
}
}
}
return collectedTypes;
}
private static void collectVariablesForPsiElement(@NotNull Function method, @NotNull Map<String, PsiVariable> collectedTypes, @NotNull PsiElement psiElement) {
if(psiElement instanceof ArrayCreationExpression) {
// reuse array collector: ['foobar' => $var]
collectedTypes.putAll(getTypesOnArrayHash((ArrayCreationExpression) psiElement));
} else if(psiElement instanceof Variable) {
// reuse variable collector: [$var]
PsiElement resolvedVariable = ((Variable) psiElement).resolve();
if(resolvedVariable instanceof Variable) {
collectedTypes.putAll(collectOnVariableReferences(method, (Variable) resolvedVariable));
}
}
}
/**
* search for possible variables which are possible accessible inside rendered twig template
*/
@NotNull
private static List<PsiElement> collectPossibleTemplateArrays(@NotNull Function method) {
List<PsiElement> collectedTemplateVariables = new ArrayList<>();
// Annotation controller
// @TODO: check for phpdoc tag
for(PhpReturn phpReturn : PsiTreeUtil.findChildrenOfType(method, PhpReturn.class)) {
PhpPsiElement returnPsiElement = phpReturn.getFirstPsiChild();
// @TODO: think of support all types here
// return $template
// return array('foo' => $var)
if(returnPsiElement instanceof Variable || returnPsiElement instanceof ArrayCreationExpression) {
collectedTemplateVariables.add(returnPsiElement);
}
}
// twig render calls:
// $twig->render('foo', $vars);
for(MethodReference methodReference : PsiTreeUtil.findChildrenOfType(method, MethodReference.class)) {
if(new Symfony2InterfacesUtil().isTemplatingRenderCall(methodReference)) {
PsiElement templateParameter = PsiElementUtils.getMethodParameterPsiElementAt((methodReference).getParameterList(), 1);
if(templateParameter != null) {
collectedTemplateVariables.add(templateParameter);
}
}
}
return collectedTemplateVariables;
}
/**
* search for references of variable declaration and collect the types
*
* @param function should be function / method scope
* @param variable the variable declaration psi $var = array();
*/
@NotNull
private static Map<String, PsiVariable> collectOnVariableReferences(@NotNull Function function, @NotNull Variable variable) {
Map<String, PsiVariable> collectedTypes = new HashMap<>();
for (Variable scopeVar : PhpElementsUtil.getVariablesInScope(function, variable)) {
PsiElement parent = scopeVar.getParent();
if (parent instanceof ArrayAccessExpression) {
// $template['variable'] = $foo
collectedTypes.putAll(getTypesOnArrayIndex((ArrayAccessExpression) parent));
} else if (parent instanceof AssignmentExpression) {
// array('foo' => $var)
if (((AssignmentExpression) parent).getValue() instanceof ArrayCreationExpression) {
collectedTypes.putAll(getTypesOnArrayHash((ArrayCreationExpression) ((AssignmentExpression) parent).getValue()));
}
}
}
return collectedTypes;
}
/**
* $template['var'] = $foo
*/
private static Map<String, PsiVariable> getTypesOnArrayIndex(ArrayAccessExpression arrayAccessExpression) {
Map<String, PsiVariable> collectedTypes = new HashMap<>();
ArrayIndex arrayIndex = arrayAccessExpression.getIndex();
if(arrayIndex != null && arrayIndex.getValue() instanceof StringLiteralExpression) {
String variableName = ((StringLiteralExpression) arrayIndex.getValue()).getContents();
Set<String> variableTypes = new HashSet<>();
PsiElement parent = arrayAccessExpression.getParent();
if(parent instanceof AssignmentExpression) {
PsiElement arrayValue = ((AssignmentExpression) parent).getValue();
if(arrayValue instanceof PhpTypedElement) {
variableTypes.addAll(((PhpTypedElement) arrayValue).getType().getTypes());
}
collectedTypes.put(variableName, new PsiVariable(variableTypes, ((AssignmentExpression) parent).getValue()));
} else {
collectedTypes.put(variableName, new PsiVariable(variableTypes, null));
}
}
return collectedTypes;
}
/**
* array('foo' => $var, 'bar' => $bar)
*/
public static Map<String, PsiVariable> getTypesOnArrayHash(ArrayCreationExpression arrayCreationExpression) {
Map<String, PsiVariable> collectedTypes = new HashMap<>();
for(ArrayHashElement arrayHashElement: arrayCreationExpression.getHashElements()) {
if(arrayHashElement.getKey() instanceof StringLiteralExpression) {
String variableName = ((StringLiteralExpression) arrayHashElement.getKey()).getContents();
Set<String> variableTypes = new HashSet<>();
if(arrayHashElement.getValue() instanceof PhpTypedElement) {
variableTypes.addAll(((PhpTypedElement) arrayHashElement.getValue()).getType().getTypes());
}
collectedTypes.put(variableName, new PsiVariable(variableTypes, arrayHashElement.getValue()));
}
}
return collectedTypes;
}
}