/*******************************************************************************
* Copyright (c) 2010 Thiago Tonelli Bartolomei.
* 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:
* Thiago Tonelli Bartolomei - initial API and implementation
******************************************************************************/
package ca.uwaterloo.gsd.fsml.javaMappingInterpreter.analysis.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConstructorInvocation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.JavaMappingInterpreterPlugin;
import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.analysis.ASTUtils;
import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.analysis.IHierarchicalCallGraphManager;
import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.analysis.IIncrementalTypeHierarchy;
import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.analysis.IJavaASTManager;
import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.analysis.JavaModelUtils;
import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.preferences.JavaPreferenceConstants;
import ca.uwaterloo.gsd.fsml.stats.Measurements;
import ca.uwaterloo.gsd.fsml.stats.Stats;
/**
* A Hierarchical Call Graph Manager implements data structures and methods to keep track
* of call graphs related only to types of the same hierarchy. When a type is added to the
* manager, its whole hierarchy will be analyzed with respect to the method declarations and
* the possible call sequences they entail.
*
* Note that although this manager deals with call graphs and is able to find the exact method
* that will be called in a given context (i.e., it understands polymorphism), it does NOT deal
* with control flow. This means that if there is a path in a method declaration that calls another
* method in the hierarchy, the declaring method is assumed to call that method in any path.
*
* @author Thiago Tonelli Bartolomei <ttonelli@gsd.uwaterloo.ca>
*/
public class HierarchicalCallGraphManager implements IHierarchicalCallGraphManager {
// some keys for performance measurements
public static final String ADDING_KEY = "HierarchicalCallGraphManager.adding";
public static final String QUERYING_KEY = "HierarchicalCallGraphManager.querying";
// some flags
protected boolean fullCallGraph = false;
/**
* If true, the created call graph will also have links for polymorphic calls that
* go outside the hierarchy.
*/
public boolean fullCallGraph() {
return JavaMappingInterpreterPlugin.getPlugin().getPreferenceStore().getString(JavaPreferenceConstants.CALL_GRAPH_TYPE_ID).equals(
JavaPreferenceConstants.FULL_VALUE);
}
protected boolean preciseCallGraph = false;
/**
* This Flag is just considered if FULL_CALL_GRAPH is true.
*
* If true, it means that a link for a polymorphic call is created only if it is precise,
* i.e., only if there is a single possibility for the target method in the project.
*/
public boolean preciseCallGraph() {
return JavaMappingInterpreterPlugin.getPlugin().getPreferenceStore().getString(JavaPreferenceConstants.CALL_GRAPH_TYPE_ID).equals(
JavaPreferenceConstants.PRECISE_VALUE);
}
protected boolean callGraphScope = false;
/**
* If true, the call graph will record method calls to a preconfigured set of methods during call graph creation.
*/
public boolean callGraphScope() {
return JavaMappingInterpreterPlugin.getPlugin().getPreferenceStore().getString(JavaPreferenceConstants.METHOD_CALLS_SEARCH_SCOPE_ID).equals(
JavaPreferenceConstants.CALL_GRAPH_VALUE);
}
/**
* For a given type, the list of methods it overrides from its super type
* TODO - this is currently not used, maybe could be removed
*/
//protected Map<IType, List<MethodTuple>> overrides = new HashMap<IType, List<MethodTuple>>();
/**
* For a given type, the list of methods it inherits from super types
*/
protected Map<IType, List<IMethod>> inherits = new HashMap<IType, List<IMethod>>();
/**
* For a given type, its implicit default constructor, if it exists
*/
protected Map<IType, IMethod> implicitConstructor = new HashMap<IType, IMethod>();
/**
* For a given member, the list of anonymous types it declares
*/
protected Map<IMember, List<IType>> anonymousTypes = new HashMap<IMember, List<IType>>();
/**
* The list of types that were already processed
*/
protected List<IType> processedTypes = new ArrayList<IType>();
/**
* Responsible for keeping calls depending on context
*/
protected final ContextualCallsManager callsManager = new ContextualCallsManager(this);
/**
* An Java AST Manager
*/
protected IJavaASTManager astManager = null;
/**
* A visitor which is reusable across visits
*/
protected InvocationSearchVisitor visitor = null;
/**
* Keep the set of binding errors found during visiting
*/
protected Set<String> errors = new HashSet<String>();
/**
* Keeps track of the type hierarchy
*/
protected IIncrementalTypeHierarchy hierarchy = null;
/**
* Creates a new graph manager that will use this AST manager
*
* @param astManager
*/
public HierarchicalCallGraphManager(IJavaASTManager astManager, IIncrementalTypeHierarchy hierarchy) {
this.astManager = astManager;
this.hierarchy = hierarchy;
}
/**
* Called before starting the analysis, so that the manager has a chance to initialize
*/
public void init() {
this.fullCallGraph = fullCallGraph();
this.preciseCallGraph = preciseCallGraph();
this.callGraphScope = callGraphScope();
this.callsManager.init();
}
/**
* Clears all cached information in this manager
*/
public synchronized void finish() {
for(String error : errors) {
Stats.INSTANCE.logError("HierarchicalCallGraphManager: " + error);
}
errors.clear();
this.finishMeasurements();
//overrides.clear();
inherits.clear();
implicitConstructor.clear();
anonymousTypes.clear();
processedTypes.clear();
callsManager.clear();
}
/**
* Prints results on performance measurements
*/
protected void finishMeasurements() {
Stats.INSTANCE.printMessage("\nHierarchical Call Graph Statistics");
Measurements.instance(ADDING_KEY).printAndReset("Adding Compilation Units : ");
Measurements.instance(QUERYING_KEY).printAndReset("Querying Method Calls : ");
Stats.INSTANCE.printMessage("");
}
/**
* Gets the method that really is going to be executed at runtime if the target method
* is called in this context type.
*
* This method will check if the target method was overridden somewhere in the hierarchy
* of the context. If the method was not overridden, target itself will be returned.
* Otherwise, the method that is available to objects of the context type will be returned.
*
* The target method must be a method in the hierarchy of the context.
*
* If the context is an interface, the single implementation of the target is returned.
* If multiple implementations exist, null is returned.
*
* @param context the context type (the type of the object receiving the call)
* @param target the method that is being called (can be retrieved from the method binding)
* @return the available method, i.e., the method that will be actually executed at runtime
* (can be target itself) or null if the method could not be found.
*/
public synchronized IMethod getExecutingMethod(IType context, IMethod target) {
try {
// return itself, if the target is private or static
if (Modifier.isStatic(target.getFlags()) ||
Modifier.isPrivate(target.getFlags()) ||
target.isConstructor()) {
return target;
}
IType declaringType = target.getDeclaringType();
for (IType superIType : hierarchy.getSupertypes(context))
if (superIType.equals(declaringType))
// tries to find the polymorphic method
return this.findMethod(context, null, target);
} catch (JavaModelException e) {
e.printStackTrace();
}
return null;
}
/**
* Return the list of methods available in this type, i.e., the methods
* declared in this type plus the ones it inherits from super types.
*
* Note that if you just want to check if a certain method is available in the
* type, you should use isMethodAvailable, which is much more efficient.
*
* This method will process the compilation unit of the type if necessary.
*
* @param type the source type
* @return the list of available methods
* @throws JavaModelException if cannot list the methods of this type
*/
public synchronized List<IMethod> getAvailableMethods(IType type) throws JavaModelException {
if (! this.processedTypes.contains(type)) {
this.addCompilationUnit(JavaModelUtils.getTypeRoot(type));
}
List<IMethod> list = new ArrayList<IMethod>();
list.addAll(Arrays.asList(type.getMethods()));
list.addAll(inherits.get(type));
return list;
}
/**
* Checks if the given method is available in the given type, i.e., if it is between
* the methods declared or inherited by the type.
*
* @param type the source type
* @param method the method to search for
* @return true, if the method is between the available methods, false otherwise.
* @throws JavaModelException
*/
public synchronized boolean isMethodAvailable(IType type, IMethod method) throws JavaModelException {
for (IMethod m : type.getMethods()) {
if (method.equals(m)) {
return true;
}
}
if (! this.processedTypes.contains(type)) {
this.addCompilationUnit(JavaModelUtils.getTypeRoot(type));
}
return inherits.containsKey(type) && inherits.get(type).contains(method);
}
/**
* Checks if there is a path in the call graph between this caller and this callee.
*
* This method will use the caller's declaring type as context
*
* @param caller the method calling
* @param callee the method being called
* @return true if there is a possibility for callee being called from caller, false otherwise
*/
public synchronized boolean calls(IMethod caller, IMethod callee) {
return calls(caller.getDeclaringType(), caller, callee);
}
/**
* Checks if there is a path in the call graph between this caller and this callee, having this
* type as a context.
*
* @param context the context type
* @param caller the method calling
* @param callee the method being called
* @return true if there is a possibility for callee being called from caller, false otherwise
*/
public synchronized boolean calls(IType context, IMethod caller, IMethod callee) {
return callsManager.calls(context, caller, callee);
}
/**
* Checks if there is a path in the call graph between any method in this context and the callee
*
* @param context the context type
* @param callee the method being called
* @return true if there is a possibility for callee being called from caller, false otherwise
*/
public synchronized boolean calls(IType context, IMethod callee) {
Measurements.instance(QUERYING_KEY).start();
try {
// check if some of the declared methods calls
for(IMethod method : context.getMethods()) {
if (calls(context, method, callee)) return true;
}
// maybe some of the inherited
if (inherits.containsKey(context)) {
for(IMethod method : inherits.get(context)) {
if (calls(context, method, callee)) return true;
}
}
// or even an implicit default constructor
if (implicitConstructor.containsKey(context)) {
if (calls(context, implicitConstructor.get(context), callee))
return true;
}
// or also in some inner type
for(IType nested : context.getTypes()) {
if (! nested.isAnonymous() && ! Modifier.isStatic(nested.getFlags())) {
if (calls(nested, callee)) {
return true;
}
}
}
// TODO - check for anonymous types that are reachable too
} catch (JavaModelException e) {
e.printStackTrace();
} finally {
Measurements.instance(QUERYING_KEY).stop();
}
return false;
}
/**
* Checks if there is a path in the call graph between this caller and this callee.
*
* If the caller is an IType, we search for all methods in the type (and inherited). If it
* is an IMethod, we search only for a connection between this method and the callee.
*
* @param caller the method or type calling
* @param callee the method being called
* @return true if there is a possibility for callee being called from caller, false otherwise
*/
public synchronized boolean calls(IMember caller, IMethod callee) {
if (caller instanceof IType) {
return calls((IType) caller, callee);
}
if (caller instanceof IMethod) {
return calls((IMethod) caller, callee);
}
return false;
}
/**
* Adds the compilation unit indicated by this type root to the manager.
*
* The Type Root can be an ICompilationUnit or an IClassFile. In any case, this method
* will analyze the inheritance hierarchy of the class regarding the methods that are
* inherited, overridden, etc. A certain hierarchy will be analyzed only once and the
* results will be cached (until a clear() is called in the manager).
*
* @param root the compilation unit to be added
*/
public synchronized void addCompilationUnit(ITypeRoot root) {
try {
Measurements.instance(ADDING_KEY).start();
IType primary = root.findPrimaryType();
if (primary != null) {
processTypeHierarchy(primary);
}
} finally {
Measurements.instance(ADDING_KEY).stop();
}
}
/**
* Gets a type hierarchy for this type and processes it regarding available methods.
*
* This method will create a type hierarchy for the given type, will get its top type
* (Object) and will process this hierarchy top-down, analyzing for each type the
* available methods (checking for inheritance and overridden methods).
*
* @param type the type whose hierarchy is going to be processed
*/
protected void processTypeHierarchy(IType type) {
if (type != null && ! processedTypes.contains(type)) {
// check if it is a buggy anonymous
if (hierarchy.contains(type) && hierarchy.getNode(type).isBuggyAnonymous()) {
// TODO - for now, just ignore it
return;
}
try {
hierarchy.addSuperTypeHierarchy(type);
processTypes(hierarchy.getBranch(type));
} catch (JavaModelException e) {
e.printStackTrace();
Stats.INSTANCE.logError("HierarchicalCallGraphManager: Could not process type hierarchy for " + type.getElementName() + " : " + e.getMessage());
}
}
}
/**
* Process the hierarchies of this list of types
*
* @param types
*/
protected void processTypeHierarchies(List<IType> types) {
for(IType type : types) {
processTypeHierarchy(type);
}
}
/**
* Processes the stack of types top-down
*
* @param top
* @throws JavaModelException
*/
protected void processTypes(Stack<IType> types) throws JavaModelException {
while(! types.isEmpty()) {
IType type = types.pop();
// process this type, if we didn't before
if (! processedTypes.contains(type)) {
processedTypes.add(type);
processTypeMethods(type);
// process the hierarchies of the nested types
for(IType nestedType : type.getTypes()) {
processTypeHierarchy(nestedType);
}
}
// go to the next sub type...
}
}
/**
* Processes the methods of this type (calculating inherited and overridden)
*
* The idea is to calculate the following formulas:
*
* super.available = super.declares (not private/static) + super.inherits
* this.inherits = super.available - overridden
*
* @param type
* @throws JavaModelException
*/
protected void processTypeMethods(IType type) throws JavaModelException {
IType sup = hierarchy.getSuperclass(type);
if (sup != null) {
// process methods declared by the super type
for(IMethod method : sup.getMethods()) {
processMethod(type, method);
}
// add the methods that the super type had inherited
if (inherits.containsKey(sup)) {
for(IMethod method : this.inherits.get(sup)) {
processMethod(type, method);
}
}
}
// now visit the methods declared in the type
visitType(type);
// process anonymous types declared in the type
processAnonymousTypes(type);
// and resolve bindings of all available methods in this context
resolveBindings(type);
}
/**
* Adds this method to the appropriate list for this type (if overridden or inherited)
*
* @param type
* @param method
* @throws JavaModelException
*/
protected void processMethod(IType type, IMethod method) throws JavaModelException {
// static, private and constructors are discarded
if (Modifier.isStatic(method.getFlags()) ||
Modifier.isPrivate(method.getFlags()) ||
method.isConstructor()) {
return;
}
if (type.findMethods(method) != null) {
// TODO overrides
//getOverrides(type).add(new MethodTuple(type.findMethods(method)[0], method));
} else {
// inherits
getInherits(type).add(method);
}
}
/**
* Visits all methods that are available in this type, which include declared,
* inherited and implicit constructors. For each of these methods, this method
* constructs the AST and parses to check for method calls. The method calls
* that are found are informed to the Contexts manager.
*
* Note that binary types are not visited and that this method recursively visits
* the sub types found in the type hierarchy.
*
* @param type
* @throws JavaModelException
*/
protected void visitType(IType type) throws JavaModelException {
if (type == null) return;
// we cannot visit binary types without sources
if (type.isStructureKnown()) {
// visit this type, looking for method calls
CompilationUnit unit = astManager.getCompilationUnit(type);
for (IMethod method : type.getMethods()) {
// this guarantees the method really exists in source
if (JavaModelUtils.hasSourceRange(method)) {
InvocationSearchVisitor v = getVisitor(type, method);
if (! type.isAnonymous())
visitor.b = ASTUtils.getTypeDeclarationNode(type, unit).resolveBinding();
MethodDeclaration decl = ASTUtils.getMethodDeclarationNode(method, unit);
decl.accept(v);
checkConstructorCall(decl, callsManager, type, method);
}
}
// and make sure this type calls a super constructor if needed
if (JavaModelUtils.hasImplicitDefaultConstructor(type)) {
IMethod constructor =
JavaModelUtils.getConcreteDefaultConstructor(hierarchy, type);
if (constructor != null) {
implicitConstructor.put(type, constructor);
}
}
}
}
/**
* Checks if the constructor call:
* 1) is a constructor (does nothing if not)
* 2) has a call to another constructor (does nothing if yes)
*
* If this constructor does NOT call another constructor, we search for the default
* constructor that should be called and add this call to the contexts manager.
*
* @param decl
* @param contexts
* @param type
* @param constructor
* @throws JavaModelException
*/
protected void checkConstructorCall(
MethodDeclaration decl,
ContextualCallsManager contexts,
IType type,
IMethod constructor) throws JavaModelException {
if (! constructor.isConstructor()) return;
Statement st = ASTUtils.getFirstStatement(decl);
if (st == null ||
! (st instanceof SuperConstructorInvocation) && st instanceof ConstructorInvocation) {
IMethod concrete =
JavaModelUtils.getConcreteDefaultConstructor(hierarchy, hierarchy.getSuperclass(type));
if (concrete != null)
contexts.addStaticCall(constructor, concrete);
}
}
protected void processAnonymousTypes(IType type) throws JavaModelException {
if (hierarchy.getNode(type).hasAnonymousType()) {
for (IMethod method : type.getMethods()) {
if (anonymousTypes.containsKey(method)) {
processTypeHierarchies(anonymousTypes.get(method));
}
}
}
}
/**
* Resolve the bindings for method calls performed by this type.
*
* This method will use this type as context and will try to resolve the bindings
* for method calls performed by methods declared and inherited by this type.
*
* @param type
* @throws JavaModelException
*/
protected void resolveBindings(IType type) throws JavaModelException {
// first the methods declared in the type
for (IMethod method : type.getMethods()) {
if (callsManager.hasBindings(method)) {
resolveBindings(type, method);
}
}
// then the methods it inherited
if (inherits.containsKey(type)) {
for (IMethod method : getInherits(type)) {
if (callsManager.hasBindings(method)) {
resolveBindings(type, method);
}
}
}
}
/**
* Resolve the bindings of polymorphic calls made by this caller, now using this context type.
*
* The bindings that can be resolved are going to be reported to the contexts manager.
*
* @param context
* @param caller
*/
public void resolveBindings(IType context, IMethod caller) {
for(IMethodBinding binding : callsManager.getBindings(caller)) {
IMethod target = findMethod(context, caller, binding);
if (target != null)
callsManager.addCall(context, caller, target);
else
Stats.INSTANCE.logError("HierarchicalCallGraphManager: Could not resolve " + binding.getName() + " in the context " + context.getFullyQualifiedName());
}
}
/**
* Helper method that gets the IMethod from the binding and calls findMethod with it.
*
* @param context
* @param caller
* @param binding
* @return
*/
protected IMethod findMethod(IType context, IMethod caller, IMethodBinding binding) {
if(context == null || binding == null) return null;
try {
return findMethod(context, caller, (IMethod) binding.getJavaElement());
} catch (JavaModelException e) {
e.printStackTrace();
return null;
}
}
/**
* In the given context, find the IMethod that corresponds to the target.
*
* In the context, the target IMethod could have been overridden by another
* method. So we search for the methods declared in the context for methods
* that override target.
*
* If not found, we search for the inherited methods.
*
* Also, there is the case when the target method is actually a private
* method in the context of the caller (when the caller gets inherited by
* the context and calls a private method). So we also search in the context
* of the caller's context
*
* @param context
* @param caller
* @param target
* @return the real IMethod that corresponds to target in this context, or null
* if not found.
*/
protected IMethod findMethod(IType context, IMethod caller, IMethod target) throws JavaModelException {
//// FIRST look in the same hierarchy
//// Note that even "private in the caller context" has precedence over outer types!
// check if it was declared in the context
for (IMethod method : context.getMethods()) {
if (method.isSimilar(target)) return method;
}
// check if it was inherited by the context
if (inherits.containsKey(context)) {
for (IMethod method : getInherits(context)) {
if (method.isSimilar(target)) return method;
}
}
// check if it was private to the caller context
if (caller != null) {
IType callerContext = caller.getDeclaringType();
if (! callerContext.getTypeQualifiedName().equals(context.getTypeQualifiedName())) {
for (IMethod method : callerContext.getMethods()) {
if (Modifier.isPrivate(method.getFlags()) && method.isSimilar(target))
return method;
}
}
}
//// SECOND start looking for outer types
// check if there is an outer type
IType outer = hierarchy.getNode(context).getDeclaringType();
//IType outer = JavaModelUtils.getDeclaringType(context);
if (outer != null && ! outer.equals(context)) {
IMethod method = findMethod(outer, caller, target);
if (method != null) {
return method;
}
}
// either we have a problem in the implementation, or somebody is asking for a method call
// whose target is outside the hierarchy
/*
String name = context.getElementName();
if (context.isAnonymous())
name = context.getFullyQualifiedName();
Stats.INSTANCE.logError(
"HierarchicalCallGraph.findMethod() could not determine an available method for " +
target.getElementName() + " in the context " + name);
*/
return null;
}
/**
* Finds the possible implementations of this method (which was found on this type) on the hierarchy
* of this type.
*
* If the method is an actual implementation of itself (i.e., if it is not abstract or
* inside an interface), then it will also be returned in the list.
*
* @param type
* @param method
* @return
*/
public List<IMethod> findImplementations(IType type, IMethod method) {
List<IMethod> methods = new ArrayList<IMethod>();
if(type == null || method == null) return methods;
List<IType> types = new ArrayList<IType>();
types.add(type);
types.addAll(Arrays.asList(hierarchy.getExhaustiveSubclasses(type)));
for (IType subType : types) {
try {
// no implementations in interfaces
if (subType.isInterface()) continue;
IMethod impl = findMethod(subType, null, method);
if (impl != null && ! Modifier.isAbstract(impl.getFlags()) && ! methods.contains(impl)) {
methods.add(impl);
}
} catch (JavaModelException e) {
e.printStackTrace();
}
}
//Stats.INSTANCE.printMessage("Found " + methods.size() + " implementations of " + method + " : " + methods);
return methods;
}
/**
* Gets a visitor already configured with this context and caller
*
* @param context
* @param caller
* @return
*/
protected InvocationSearchVisitor getVisitor(IType context, IMethod caller) {
getVisitor().configure(context, caller);
return visitor;
}
/**
* Gets a visitor, creating if needed
*
* @return
*/
protected InvocationSearchVisitor getVisitor() {
if (visitor == null) {
visitor = new InvocationSearchVisitor(callsManager);
}
return visitor;
}
/**
* Gets the list of inherited methods for this type, creating
* the list if needed.
*
* @param type
* @return
*/
protected List<IMethod> getInherits(IType type) {
if (! inherits.containsKey(type)) {
inherits.put(type, new ArrayList<IMethod>());
}
return inherits.get(type);
}
/**
* Gets the list of overridden methods for this type, creating
* the list if needed.
*
* @param type
* @return
*/
/* TODO - OVERRIDES!
protected List<MethodTuple> getOverrides(IType type) {
if (! overrides.containsKey(type)) {
overrides.put(type, new ArrayList<MethodTuple>());
}
return overrides.get(type);
}*/
/**
* Gets the list of anonymous types declared in this method,
* creating the list if needed.
*
* @param method
* @return
*/
protected List<IType> getAnonymousTypes(IMethod method) {
if (! anonymousTypes.containsKey(method)) {
anonymousTypes.put(method, new ArrayList<IType>());
}
return anonymousTypes.get(method);
}
/**
* This visitor searches the AST for method calls.
*
* The method calls can be:
*
* - super method invocation : super.m();
* - super constructor invocation : super();
* - constructor invocation : this();
* - method invocation : m();
*
* The invocations found are reported to the Contexts manager.
*
* Note that method invocations are only dealt with when they are "this", i.e.,
* the target is a method in the hierarchy of the declaring type.
*
* @author Thiago Tonelli Bartolomei <ttonelli@gsd.uwaterloo.ca>
*/
public class InvocationSearchVisitor extends ASTVisitor {
protected ContextualCallsManager contexts = null;
protected IType contextType = null;
protected IMethod caller = null;
protected ITypeBinding b = null;
public InvocationSearchVisitor(ContextualCallsManager contexts) {
this.contexts = contexts;
}
/**
* Configures the visitor for a new visit, reseting the dynamic calls flag
*
* @param context
* @param caller
*/
public void configure(IType context, IMethod caller) {
this.contextType = context;
this.caller = caller;
}
/**
* Visits an instance creation, adding to the context
*/
public boolean visit(ClassInstanceCreation node) {
IMethodBinding binding = node.resolveConstructorBinding();
if (binding != null) {
IMethod target = (IMethod) binding.getJavaElement();
if (target == null) {
// maybe we already know its implicit constructor
IType targetType = (IType) binding.getDeclaringClass().getJavaElement();
target = implicitConstructor.get(targetType);
if (target == null) {
// maybe not... need to resolve it
target =
JavaModelUtils.getConcreteDefaultConstructor(hierarchy, targetType);
}
}
if (target != null)
contexts.addStaticCall(caller, target);
} else
errors.add(
"Could not resolve instance creation for " +
contextType.getElementName() + "-> new " + node.getExpression() + "(...)");
return true;
}
/**
* Visits the super method invocation, adding to the context
*/
public boolean visit(SuperMethodInvocation node) {
IMethodBinding binding = node.resolveMethodBinding();
if (binding != null)
contexts.addStaticCall(caller, (IMethod) binding.getJavaElement());
else
errors.add(
"Could not resolve super method binding for " +
contextType.getElementName() + "-> super." + node.getName() + "(...)");
return true;
}
/**
* Visits the super constructor invocation, adding to the context
*/
public boolean visit(SuperConstructorInvocation node) {
IMethodBinding binding = node.resolveConstructorBinding();
if (binding != null)
contexts.addStaticCall(caller, (IMethod) binding.getJavaElement());
else
errors.add(
"Could not resolve super constructor binding for " +
contextType.getElementName() + "-> super(..)");
return true;
}
/**
* Visits the constructor invocation, adding to the context
*/
public boolean visit(ConstructorInvocation node) {
IMethodBinding binding = node.resolveConstructorBinding();
if (binding != null)
contexts.addStaticCall(caller, (IMethod) binding.getJavaElement());
else
errors.add(
"Could not resolve constructor binding for " +
contextType.getElementName() + "-> new " + node.toString() + "(..)");
return true;
}
/**
* Visits the method invocation, adding to the context if it is "this"
*/
public boolean visit(MethodInvocation node) {
IMethodBinding binding = node.resolveMethodBinding();
if (binding != null) {
if (callGraphScope) {
// Michal: related to the experimental callGraph scope
// add to the target method calls list
IMethod iMethod = (IMethod) binding.getJavaElement();
List<Expression> calls = targetCalls.get(iMethod);
if (calls != null)
calls.add(node);
}
// static and final invocations are always bound correctly
if (
Modifier.isStatic(binding.getModifiers()) ||
Modifier.isFinal(binding.getModifiers()) ||
Modifier.isFinal(binding.getDeclaringClass().getModifiers())) {
contexts.addStaticCall(caller, (IMethod) binding.getJavaElement());
} else {
// check if it is a "this" call
Expression s = node.getExpression();
if (s == null || s instanceof ThisExpression) {
contexts.addBinding(caller, binding);
} else {
if (fullCallGraph || preciseCallGraph) {
List<IMethod> implementations = findImplementations(
(IType) binding.getDeclaringClass().getJavaElement(),
(IMethod) binding.getJavaElement());
if (! preciseCallGraph || implementations.size() == 1) {
for(IMethod implementation : implementations) {
// add static links to all possible implementations
contexts.addStaticCall(caller, implementation);
}
}
}
}
}
} else {
String expr = node.getExpression() != null ? node.getExpression().toString() : "this";
errors.add(
"Could not resolve method binding for " +
contextType.getElementName() + "-> " + expr + "." + node.getName() + "(...)");
}
return false;
}
/**
* Don't go internally on anonymous classes since they cause problems
* to the analysis (eg., methods inside those classes should have a different
* context than the context in this visitor)
*/
public boolean visit(AnonymousClassDeclaration node) {
ITypeBinding binding = node.resolveBinding();
if (binding != null) {
IType anonymous = (IType) binding.getJavaElement();
try {
hierarchy.addSuperTypeHierarchy(anonymous);
getAnonymousTypes(caller).add(anonymous);
} catch (JavaModelException e) {
errors.add("Found buggy anonymous type at : " + caller);
try {
hierarchy.addBuggyAnonymousType(
anonymous, binding.getSuperclass(), binding.getInterfaces());
} catch (JavaModelException e2) {
e2.printStackTrace();
}
}
hierarchy.addAnonymousType(contextType, anonymous);
} else {
errors.add("Could not resolve type binding for anonymous class in " +
contextType.getElementName() + "." + caller.getElementName() + "(...)");
}
return false;
}
/**
* Never visit nested types
*/
public boolean visit(TypeDeclaration node) {
return false;
}
}
HashMap<IMethod, List<Expression>> targetCalls;
/**
* Michal: related to the experimental 'callGraph' search scope
* This method should be called before the analysis starts to set the methods for which method calls will be recorded.
* @param targetMethods
*/
public void setTargetMethods(Collection<IMethod> targetMethods) {
targetCalls = new HashMap<IMethod, List<Expression>>();
for (IMethod method : targetMethods) {
targetCalls.put(method, new LinkedList<Expression>());
}
}
/**
* Michal: related to the experimental 'callGraph' search scope
* @param targetMethod
* @return method invocations of the given target method reachable in the call graph.
*/
public List<Expression> getCalls(IMethod targetIMethod, IType contextIType) {
if (targetCalls != null)
return targetCalls.get(targetIMethod);
return null;
}
}