/*******************************************************************************
* Copyright (c) 2012 VMWare, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMWare, Inc. - initial API and implementation
*******************************************************************************/
package org.grails.ide.eclipse.search;
import java.util.HashSet;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.jdt.groovy.model.GroovyCompilationUnit;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.search.ui.text.Match;
import org.grails.ide.eclipse.core.GrailsCoreActivator;
/**
* Abstract superclass for code visitors that are used to find stuff in Groovy AST.
*
* @author Kris De Volder
*
* @since 2.8
*/
public abstract class SearchingCodeVisitor extends ClassCodeVisitorSupport {
/**
* Action that is called by visitor when it finds a method call node that has a
* given method name.
*/
public static abstract class MethodCallAction {
//Consider using generics if more node types need to be supported:
//NodeAction<N extends ASTNode>
public final String methodName;
public MethodCallAction(String methodName) {
Assert.isLegal(methodName!=null);
this.methodName = methodName;
}
/**
* This method is called by the visitor when it finds a call node where the
* target method name matched the String we expect.
* <p>
* The visitor is responsible to make sure the same node is not visited twice
* and to ensure that the method being passed to this method has the stipulated
* method name.
*/
public abstract void doit(SearchingCodeVisitor visitor, MethodCallExpression call);
}
/**
* An abstract MethodCallAction that matches calls of the form and delegates to
* an abstratc 'matchFound' method when the pattern is matched.
*/
public static abstract class FindNamedArgumentAction extends MethodCallAction {
public final String argName;
public final String oldValue;
public FindNamedArgumentAction(String methodName, String argName, String oldValue) {
super(methodName);
this.argName = argName;
this.oldValue = oldValue;
}
public String toString() {
return methodName +"(... "+argName+": "+"\""+oldValue+"\" ...)";
}
public void doit(SearchingCodeVisitor visitor, MethodCallExpression call) {
//Recognize and handle <methodName>(..., <argName>: "<oldName>", ...)
MapEntryExpression arg = SearchUtil.getNamedArgument(call, argName);
if (arg!=null) {
final Expression valueExpression = arg.getValueExpression();
String argValue = SearchUtil.getStringValue(valueExpression);
if (oldValue.equals(argValue)) {
matchFound(visitor, call, valueExpression);
}
}
}
/**
* Override to implement an action that is executed only when a match to the pattern is found.
* @param call -- the call containing the argument
* @param valueExpression -- the expression corresponding to the argument's value.
*/
protected abstract void matchFound(SearchingCodeVisitor visitor, MethodCallExpression call, Expression valueExpression);
}
/**
* Bit of info that identifies a node, so we can avoid visiting same node twice.
*
* @author Kris De Volder.
* @since 2.8
*/
protected static class NodeKey {
private int pos;
private int len;
public NodeKey(ASTNode node) {
pos = node.getStart();
len = node.getLength();
}
@Override
public String toString() {
return "["+pos+","+len+"]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + len;
result = prime * result + pos;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
NodeKey other = (NodeKey) obj;
if (len != other.len)
return false;
if (pos != other.pos)
return false;
return true;
}
}
private GroovyCompilationUnit cu = null;
private String cuText;
public SearchingCodeVisitor(ICompilationUnit cu) {
if (cu instanceof GroovyCompilationUnit) {
this.cu = (GroovyCompilationUnit)cu;
this.cuText = new String(((GroovyCompilationUnit) cu).getContents());
}
}
/**
* Creates a 'match'. It also checks whether the 'found' expression's text actually
* contains the searched text and ensures to make the match cover exactly the searchedText.
*/
public Match createMatch(Expression valueExpression, String searchedText) throws CoreException {
int start = valueExpression.getStart();
int len = valueExpression.getLength();
String foundText = cuText.substring(start, start+len);
int displace = foundText.indexOf(searchedText);
if (displace>=0) {
IJavaElement el = cu.getElementAt(start);
return new Match(el, start+displace, searchedText.length());
} else {
throw new CoreException(new Status(IStatus.ERROR, GrailsCoreActivator.PLUGIN_ID,
"Found AST node, but it doesn't contain searched text"));
}
}
@SuppressWarnings("restriction")
public void start() {
if (cu instanceof GroovyCompilationUnit) {
GroovyCompilationUnit gcu = (GroovyCompilationUnit) cu;
try {
ModuleNode node = gcu.getModuleNode();
if (node==null) {
recordError("Could not obtain Groovy parse tree for '"+cu.getElementName()+"'. Fix compilation problems and try again.");
} else {
this.visit(node);
}
} finally {
this.cuText = null;
}
}
}
public void recordError(String msg) {
GrailsCoreActivator.log(msg);
}
@Override
protected SourceUnit getSourceUnit() {
return null;
}
public void visit(ModuleNode node) {
for (ClassNode clazz : node.getClasses()) {
visitClass(clazz);
}
}
@Override
public void visitMethodCallExpression(MethodCallExpression call) {
String name = call.getMethodAsString(); //Could be null (for 'funny' calls where target name is dynamic)
SearchingCodeVisitor.MethodCallAction action = getMethodCallAction(name);
if (action!=null) {
Assert.isLegal(action.methodName.equals(name));
if (!isVisited(call)) {
action.doit(this, call);
}
}
super.visitMethodCallExpression(call);
}
/**
* Concrete subclass can provide some way to determine action to take when
* a method with some given name is being visited.
*/
protected MethodCallAction getMethodCallAction(String methodName) {
return null;
}
/**
* To avoid processing same node twice. Nodes are only kept in here if they actually matter
* for the traversal (i.e. if they have actions associated with them, so this set should be
* quite small assuming the set of interesting nodes in the tree is quite small).
*/
private HashSet<NodeKey> visited = new HashSet<NodeKey>();
private ClassNode currentTopLevelClass;
/**
* @return True if the method was never called on a given node before. I.e. it returns false only the first
* time it is called on this node.
*/
private boolean isVisited(MethodCallExpression node) {
final NodeKey key = new NodeKey(node);
if (!visited.contains(key)) {
visited.add(key);
return false;
}
return true;
}
@Override
public void visitClass(ClassNode node) {
boolean isToplevel = false;
if (currentTopLevelClass==null) {
//entering top level class
currentTopLevelClass = node;
isToplevel = true;
}
try {
super.visitClass(node);
} finally {
if (isToplevel) {
//Exiting top level class
currentTopLevelClass = null;
}
}
}
public ClassNode getCurrentTopLevelClass() {
return currentTopLevelClass;
}
}