/******************************************************************************* * 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; } }