/*******************************************************************************
* This file is part of the Symfony eclipse plugin.
*
* (c) Robert Gruendler <r.gruendler@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
******************************************************************************/
package com.dubture.symfony.core.index.visitor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.dltk.ast.ASTNode;
import org.eclipse.dltk.ast.expressions.CallArgumentsList;
import org.eclipse.dltk.ast.expressions.Expression;
import org.eclipse.dltk.ast.references.SimpleReference;
import org.eclipse.dltk.ast.references.VariableReference;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.php.core.compiler.ast.nodes.ArrayCreation;
import org.eclipse.php.core.compiler.ast.nodes.ArrayElement;
import org.eclipse.php.core.compiler.ast.nodes.Assignment;
import org.eclipse.php.core.compiler.ast.nodes.ClassInstanceCreation;
import org.eclipse.php.core.compiler.ast.nodes.FormalParameter;
import org.eclipse.php.core.compiler.ast.nodes.FullyQualifiedReference;
import org.eclipse.php.core.compiler.ast.nodes.NamespaceDeclaration;
import org.eclipse.php.core.compiler.ast.nodes.PHPCallExpression;
import org.eclipse.php.core.compiler.ast.nodes.PHPMethodDeclaration;
import org.eclipse.php.core.compiler.ast.nodes.ReturnStatement;
import org.eclipse.php.core.compiler.ast.nodes.Scalar;
import org.eclipse.php.core.compiler.ast.nodes.UsePart;
import org.eclipse.php.core.compiler.ast.nodes.UseStatement;
import org.eclipse.php.core.compiler.ast.visitor.PHPASTVisitor;
import org.eclipse.php.internal.core.model.PerFileModelAccessCache;
import org.eclipse.php.internal.core.typeinference.IModelAccessCache;
import org.eclipse.wst.sse.core.utils.StringUtils;
import com.dubture.doctrine.annotation.model.Annotation;
import com.dubture.doctrine.core.AnnotationParserUtil;
import com.dubture.doctrine.core.compiler.IAnnotationModuleDeclaration;
import com.dubture.symfony.core.log.Logger;
import com.dubture.symfony.core.model.Service;
import com.dubture.symfony.core.model.SymfonyModelAccess;
import com.dubture.symfony.core.model.TemplateVariable;
import com.dubture.symfony.core.preferences.SymfonyCoreConstants;
import com.dubture.symfony.core.util.AnnotationUtils;
import com.dubture.symfony.core.util.ModelUtils;
import com.dubture.symfony.core.util.PathUtils;
import com.dubture.symfony.index.model.Route;
/**
*
* The {@link TemplateVariableVisitor} indexes collects templateVariables in
* Symfony2 controller classes.
*
*
* @author Robert Gruendler <r.gruendler@gmail.com>
*
*/
@SuppressWarnings("restriction")
public class TemplateVariableVisitor extends PHPASTVisitor {
// The templatevariables with their corresponding ViewPath
private Map<TemplateVariable, String> templateVariables = new HashMap<TemplateVariable, String>();
// The variables found during method parsing
private Stack<TemplateVariable> deferredVariables = new Stack<TemplateVariable>();
private PHPMethodDeclaration currentMethod;
final private List<UseStatement> useStatements;
final private NamespaceDeclaration namespace;
private boolean inAction = false;
private String currentAnnotationPath;
private Stack<Route> routes = new Stack<Route>();
final private String bundle;
private String controller;
private ISourceModule source;
private IModelAccessCache cache;
private IAnnotationModuleDeclaration annotationModule;
public TemplateVariableVisitor(List<UseStatement> useStatements, NamespaceDeclaration namespace, ISourceModule source) {
this.namespace = namespace;
this.useStatements = useStatements;
this.source = source;
try {
this.annotationModule = AnnotationParserUtil.getModule(source);
} catch (CoreException e) {
Logger.logException(e);
}
this.cache = new PerFileModelAccessCache(source);
bundle = ModelUtils.extractBundleName(namespace);
}
public Map<TemplateVariable, String> getTemplateVariables() {
return templateVariables;
}
/**
*
* Visit a {@link PHPMethodDeclaration} and check if it's an Action.
*
*/
@Override
public boolean visit(PHPMethodDeclaration methodDeclaration) throws Exception {
currentMethod = methodDeclaration;
deferredVariables = new Stack<TemplateVariable>();
controller = currentMethod.getDeclaringTypeName().replace(SymfonyCoreConstants.CONTROLLER_CLASS, "");
inAction = methodDeclaration.getName().endsWith(SymfonyCoreConstants.ACTION_SUFFIX);
if (!inAction) {
return false;
}
String action = currentMethod.getName().replace(SymfonyCoreConstants.ACTION_SUFFIX, "");
if (annotationModule != null) {
List<Annotation> annotations = annotationModule.readAnnotations((ASTNode)methodDeclaration).getAnnotations();
for (Annotation annotation : annotations) {
String className = annotation.getClassName();
if (className.startsWith(SymfonyCoreConstants.TEMPLATE_ANNOTATION)) {
currentAnnotationPath = AnnotationUtils.extractTemplate(annotation, bundle, controller, action);
} else if (className.startsWith(SymfonyCoreConstants.ROUTE_ANNOTATION)) {
Route route = AnnotationUtils.extractRoute(annotation, bundle, controller, action);
routes.push(route);
}
}
}
for (Object argument : currentMethod.getArguments()) {
/* public function (Form $form) { } */
if (argument instanceof FormalParameter) {
FormalParameter param = (FormalParameter) argument;
if (param.getParameterType() instanceof FullyQualifiedReference) {
FullyQualifiedReference ref = (FullyQualifiedReference) param.getParameterType();
NamespaceReference nsRef = createFromFQCN(ref);
if (nsRef != null) {
TemplateVariable tempVar = new TemplateVariable(currentMethod, param.getName(), param.sourceStart(), param.sourceEnd(), nsRef.namespace,
nsRef.className);
deferredVariables.push(tempVar);
}
/* public function ($somevar) { } */
} else {
TemplateVariable tempVar = new TemplateVariable(currentMethod, param.getName(), param.sourceStart(), param.sourceEnd(), null, null);
deferredVariables.push(tempVar);
}
}
}
return true;
}
@Override
public boolean endvisit(PHPMethodDeclaration s) throws Exception {
currentAnnotationPath = null;
deferredVariables = null;
currentMethod = null;
inAction = false;
return true;
}
public boolean endvisit(PHPCallExpression s) throws Exception {
if (!s.getName().startsWith("render"))
return false;
CallArgumentsList list = s.getArgs();
if (list.getChilds().size() > 1) {
if (list.getChilds().get(0) instanceof Scalar && list.getChilds().get(1) instanceof ArrayCreation) {
Scalar scalar = (Scalar) list.getChilds().get(0);
String viewPath = StringUtils.stripQuotes(scalar.getValue());
ArrayCreation params = (ArrayCreation) list.getChilds().get(1);
parseVariablesFromArray(params, viewPath);
}
}
return true;
}
@Override
public boolean visit(PHPCallExpression s) throws Exception {
return true;
}
/**
* Parse {@link ReturnStatement}s and try to evaluate the variables.
*
*/
@Override
public boolean visit(ReturnStatement statement) throws Exception {
// we're inside an action, find the template variables
if (inAction) {
// the simplest case:
// return array('foo' => $bar);
if (statement.getExpr().getClass() == ArrayCreation.class) {
if (namespace != null) {
String viewPath = null;
if (currentAnnotationPath != null) {
viewPath = currentAnnotationPath;
} else {
String template = currentMethod.getName().replace(SymfonyCoreConstants.ACTION_SUFFIX, "");
viewPath = String.format("%s:%s:%s", bundle, controller, template);
}
if (viewPath != null) {
ArrayCreation array = (ArrayCreation) statement.getExpr();
parseVariablesFromArray(array, viewPath);
}
}
// a render call:
// return return
// $this->render("DemoBundle:Test:index.html.twig", array('foo'
// => $bar));
} else if (statement.getExpr().getClass() == PHPCallExpression.class) {
PHPCallExpression expression = (PHPCallExpression) statement.getExpr();
String callName = expression.getCallName().getName();
if (callName.startsWith(SymfonyCoreConstants.RENDER_PREFIX)) {
CallArgumentsList args = expression.getArgs();
List<ASTNode> children = args.getChilds();
Scalar view = (Scalar) children.get(0);
if (children.size() >= 2 && children.get(1).getClass() == ArrayCreation.class) {
parseVariablesFromArray((ArrayCreation) children.get(1), PathUtils.createViewPath(view));
} else {
Logger.log(Logger.WARNING, "Unable to parse view variable from " + children.toString());
}
}
}
}
return true;
}
/**
*
* Parse the TemplateVariables from the given {@link ReturnStatement}
*
* @param viewPath
* @param statement
*/
private void parseVariablesFromArray(ArrayCreation array, String viewPath) {
for (ArrayElement element : array.getElements()) {
Expression key = element.getKey();
Expression value = element.getValue();
if (key.getClass() == Scalar.class) {
Scalar varName = (Scalar) key;
// something in the form: return array ('foo' => $bar);
// check the type of $bar:
if (value.getClass() == VariableReference.class) {
VariableReference ref = (VariableReference) value;
for (TemplateVariable variable : deferredVariables) {
// we got the variable, add it the the templateVariables
if (ref.getName().equals(variable.getName())) {
// alter the variable name
variable.setName(varName.getValue());
templateVariables.put(variable, viewPath);
break;
}
}
// this is more complicated, something like:
// return array('form' => $form->createView());
// we need to infer $form and then check the returntype of
// createView()
} else if (value.getClass() == PHPCallExpression.class) {
PHPCallExpression callExp = (PHPCallExpression) value;
VariableReference varRef = null;
try {
varRef = (VariableReference) callExp.getReceiver();
} catch (ClassCastException e) {
Logger.log(Logger.WARNING,
callExp.getReceiver().getClass().toString() + " could not be cast to VariableReference in " + currentMethod.getName());
}
if (varRef == null) {
continue;
}
SimpleReference callName = callExp.getCallName();
// we got the variable name (in this case $form)
// now search for the defferedVariable:
for (TemplateVariable deferred : deferredVariables) {
// we got it, find the returntype of the
// callExpression
if (deferred.getName().equals(varRef.getName())) {
TemplateVariable tempVar = SymfonyModelAccess.getDefault().createTemplateVariableByReturnType(source, currentMethod, callName,
deferred.getClassName(), deferred.getNamespace(), varRef.getName(), cache);
templateVariables.put(tempVar, viewPath);
break;
}
}
// this is a direct ClassInstanceCreation, ie:
// return array('user' => new User());
} else if (value.getClass() == ClassInstanceCreation.class) {
ClassInstanceCreation instance = (ClassInstanceCreation) value;
if (instance.getClassName().getClass() == FullyQualifiedReference.class) {
FullyQualifiedReference fqcn = (FullyQualifiedReference) instance.getClassName();
NamespaceReference nsRef = createFromFQCN(fqcn);
if (nsRef != null) {
TemplateVariable variable = new TemplateVariable(currentMethod, varName.getValue(), varName.sourceStart(), varName.sourceEnd(),
nsRef.getNamespace(), nsRef.getClassName());
templateVariables.put(variable, viewPath);
}
}
} else {
Logger.debugMSG("array value: " + value.getClass());
}
}
}
}
/**
*
* Collect all Assignments inside a {@link PHPMethodDeclaration} to infer
* them in the ReturnStatements and add it to the templateVariables.
*
*/
@Override
public boolean visit(Assignment s) throws Exception {
if (inAction) {
Service service = null;
if (s.getVariable().getClass() == VariableReference.class) {
VariableReference var = (VariableReference) s.getVariable();
// A call expression like $foo = $this->get('bar');
//
if (s.getValue().getClass() == PHPCallExpression.class) {
PHPCallExpression exp = (PHPCallExpression) s.getValue();
// are we requesting a Service?
if (exp.getName().equals("get")) {
service = ModelUtils.extractServiceFromCall(exp, source.getScriptProject().getPath());
if (service != null) {
String fqsn = service.getNamespace() != null ? service.getNamespace().getQualifiedName() : null;
TemplateVariable tempVar = new TemplateVariable(currentMethod, var.getName(), exp.sourceStart(), exp.sourceEnd(), fqsn,
service.getClassName());
deferredVariables.push(tempVar);
}
// a more complex expression like
// $form = $this->get('form.factory')->create(new
// ContactType());
} else if (exp.getReceiver().getClass() == PHPCallExpression.class) {
// try to extract a service if it's a Servicecontainer
// call
service = ModelUtils.extractServiceFromCall((PHPCallExpression) exp.getReceiver(), source.getScriptProject().getPath());
// nothing found, return
if (service == null || exp.getCallName() == null) {
return true;
}
SimpleReference callName = exp.getCallName();
// TODO: this is a problematic case, as during a clean
// build
// it's possible that the SourceModule in which the
// called method was declared is not yet in the index,
// so
// the return type cannot be evaluated and therefore
// the templatevariable won't be created...
//
// Possible solution: check if there's an event fired
// when the
// build is completed and store those return types in a
// global
// singleton, evaluate them when the whole build process
// is finished.
String fqsn = service.getNamespace() != null ? service.getNamespace().getQualifiedName() : null;
TemplateVariable tempVar = null;
tempVar = SymfonyModelAccess.getDefault().createTemplateVariableByReturnType(source, currentMethod, callName, service.getClassName(),
fqsn, var.getName(), cache);
if (tempVar != null) {
deferredVariables.push(tempVar);
}
// something like $formView = $form->createView();
} else if (exp.getReceiver().getClass() == VariableReference.class) {
VariableReference varRef = (VariableReference) exp.getReceiver();
SimpleReference ref = exp.getCallName();
// check for a previosly declared variable
for (TemplateVariable tempVar : deferredVariables) {
if (tempVar.getName().equals(varRef.getName())) {
TemplateVariable tVar = SymfonyModelAccess.getDefault().createTemplateVariableByReturnType(source, currentMethod, ref,
tempVar.getClassName(), tempVar.getNamespace(), var.getName(), cache);
if (tVar != null) {
deferredVariables.push(tVar);
break;
}
}
}
}
// a simple ClassInstanceCreation, ie. $contact = new
// ContactType();
} else if (s.getValue().getClass() == ClassInstanceCreation.class) {
ClassInstanceCreation instance = (ClassInstanceCreation) s.getValue();
if (instance.getClassName().getClass() == FullyQualifiedReference.class) {
FullyQualifiedReference fqcn = (FullyQualifiedReference) instance.getClassName();
NamespaceReference nsRef = createFromFQCN(fqcn);
if (nsRef != null) {
TemplateVariable variable = new TemplateVariable(currentMethod, var.getName(), var.sourceStart(), var.sourceEnd(),
nsRef.getNamespace(), nsRef.getClassName());
deferredVariables.push(variable);
}
}
} else if (s.getValue().getClass() == Scalar.class) {
TemplateVariable variable = new TemplateVariable(currentMethod, var.getName(), var.sourceStart(), var.sourceEnd(), null, null);
deferredVariables.push(variable);
}
}
}
return true;
}
/**
*
* Get the ClassName and Namespace from a {@link FullyQualifiedReference}
*
* @param fqcn
* @return
*/
private NamespaceReference createFromFQCN(FullyQualifiedReference fqcn) {
for (UseStatement use : useStatements) {
for (UsePart part : use.getParts()) {
if (part.getNamespace().getName().equals(fqcn.getName())) {
String name = fqcn.getName();
String qualifier = part.getNamespace().getNamespace().getName();
return new NamespaceReference(qualifier, name);
}
}
}
return null;
}
/**
* Simple helper class to pass around namespaces.
*/
private class NamespaceReference {
private String namespace;
private String className;
public NamespaceReference(String qualifier, String name) {
this.namespace = qualifier;
this.className = name;
}
public String getNamespace() {
return namespace;
}
public String getClassName() {
return className;
}
}
public Stack<Route> getRoutes() {
return routes;
}
}