/******************************************************************************* * 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.HashMap; import java.util.List; import java.util.Map; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.dom.IMethodBinding; import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.JavaMappingInterpreterPlugin; import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.analysis.JavaModelUtils; import ca.uwaterloo.gsd.fsml.javaMappingInterpreter.preferences.JavaPreferenceConstants; import ca.uwaterloo.gsd.fsml.stats.Stats; /** * Keeps track of the method calls that may happen for a given context (a type). * * This class is a manager for information regarding a type. Its interface defines * methods to add relationships between methods, like "in type A (the context), method * m directly calls method n and method n directly calls method q". Later, information * can be retrieved making questions such as "given a context A, can m reach q"? The * class is equipped with methods that recursively search for the connection m-n-q. * * @author Thiago Tonelli Bartolomei <ttonelli@gsd.uwaterloo.ca> */ public class ContextualCallsManager { /** * Should the manager "learn" partial (true) results to calls()? */ public static final boolean CACHE = true; /** * Should the manager "learn" partial (false) results to calls()? */ public static final boolean NEGATIVE_CACHE = true; /** * Prints the trace of matched "calls" questions, i.e., the path in the call graph that lead to the match */ public static boolean printMatchedCallsTrace() { return JavaMappingInterpreterPlugin.getPlugin().getPreferenceStore().getBoolean(JavaPreferenceConstants.PRINT_MATCHED_CALLS_TRACE_ID); } /** * Prints the trace of matched "calls" questions, i.e., the path in the call graph that lead to the match. * Note that if CACHE == true it will just work the first time, then we will "learn" that a path exists. It is * then recommended to use at least "1" as minimum path. */ public static int matchedCallsTraceLength() { return JavaMappingInterpreterPlugin.getPlugin().getPreferenceStore().getInt(JavaPreferenceConstants.MATCHED_CALLS_TRACE_LENGTH_ID); } /** * To speed up during a single call */ public boolean printMatchedCallsTrace = false; public int matchedCallsTraceLength = 1; /** * Maps a given context (type) to another map that links a given method to the list of other * methods it directly calls. */ protected Map<IType, ContextInfo> contexts = new HashMap<IType, ContextInfo>(); /** * Given a method, the list of bindings to the methods called by it (polymorphic calls). */ protected Map<IMethod, List<IMethodBinding>> bindings = new HashMap<IMethod, List<IMethodBinding>>(); /** * For a given method, keeps a list of the methods that it statically calls (including super calls) */ protected Map<IMethod, List<IMethod>> staticCalls = new HashMap<IMethod, List<IMethod>>(); /** * List of methods being processed while searching the graph */ protected List<IMethod> processingMethods = new ArrayList<IMethod>(); /** * A stack that keeps track of the path in the call graph that matched a certain call */ protected List<IMethod> callStack = new ArrayList<IMethod>(); /** * The call graph using this manager */ protected HierarchicalCallGraphManager callGraph = null; /** * Creates a new contextual calls manager * * @param callGraph */ public ContextualCallsManager(HierarchicalCallGraphManager callGraph) { this.callGraph = callGraph; } /** * Adds the information that in this context, this caller directly calls the callee. * * @param context * @param caller * @param callee */ public void addCall(IType context, IMethod caller, IMethod callee) { getMethodInfo(context, caller).addCall(callee); } /** * Adds the information that whenever we reach this caller (independently of context), it makes a * static call to the callee. The static call can really be a static method or a call to super.x(); * * * @param caller * @param callee */ public void addStaticCall(IMethod caller, IMethod callee) { List<IMethod> targets = getStaticTargets(caller); if (! targets.contains(callee)) { targets.add(callee); } } /** * Registers the information that this method calls the method pointed by this binding. * * @param caller * @param binding */ public void addBinding(IMethod caller, IMethodBinding binding) { List<IMethodBinding> bindings = getBindings(caller); if (! bindings.contains(binding)) { bindings.add(binding); } } /** * Checks if there are bindings registered for this method. * * @param caller * @return */ public boolean hasBindings(IMethod caller) { return bindings.containsKey(caller); } /** * Gets the list of bindings for this method, creating if needed. * * @param caller * @return */ public List<IMethodBinding> getBindings(IMethod caller) { List<IMethodBinding> targets = bindings.get(caller); if (targets == null) { targets = new ArrayList<IMethodBinding>(1); bindings.put(caller, targets); } return targets; } /** * Gets the list of static targets (callees) of this caller, creating * if needed. * * @param caller * @return */ protected List<IMethod> getStaticTargets(IMethod caller) { List<IMethod> targets = staticCalls.get(caller); if (targets == null) { targets = new ArrayList<IMethod>(1); staticCalls.put(caller, targets); } return targets; } /** * Gets the method info for this method in this context. This method * should be used if you want to add something to the method info. To * only check if it exists, use hasMethodInfo(context, method) * * @param context * @param method * @return */ protected MethodInfo getMethodInfo(IType context, IMethod method) { if (! contexts.containsKey(context)) { contexts.put(context, new ContextInfo()); } return contexts.get(context).getMethodInfo(method); } /** * Checks if this method, in this context, has non calls * * @param context * @param method * @return */ protected boolean hasNonCalls(IType context, IMethod method) { return contexts.containsKey(context) && contexts.get(context).hasMethodInfo(method) && contexts.get(context).getMethodInfo(method).hasNonCalls(); } /** * Checks if this method, in this context, has calls * * @param context * @param method * @return */ protected boolean hasCalls(IType context, IMethod method) { return contexts.containsKey(context) && contexts.get(context).hasMethodInfo(method) && contexts.get(context).getMethodInfo(method).hasCalls(); } /** * Gets the non calls for this method, in this context * * @param context * @param method * @return */ protected List<IMethod> getNonCalls(IType context, IMethod method) { return getMethodInfo(context, method).getNonCalls(); } /** * Gets the calls for this method, in this context * * @param context * @param method * @return */ protected List<IMethod> getCalls(IType context, IMethod method) { return getMethodInfo(context, method).getCalls(); } /** * Initializes the calls manager */ public void init() { printMatchedCallsTrace = printMatchedCallsTrace(); matchedCallsTraceLength = matchedCallsTraceLength(); } /** * Checks if there is a path between this caller and this callee in the graph, using this * context. * * @param context * @param caller * @param callee * @return */ public boolean calls(IType context, IMethod caller, IMethod callee) { try { return internalCalls(context, caller, callee); } finally { if (printMatchedCallsTrace && callStack.size() >= matchedCallsTraceLength) { printStack(caller, callee); } callStack.clear(); processingMethods.clear(); } } /** * Recursively search for a path between the caller and this callee, having this * context as the type of the object. * * This method will use the processing methods list to keep track of callers that * have been already processed, so that we don't get into an infinite loop. * * @param context * @param caller * @param callee * @return */ protected boolean internalCalls(IType context, IMethod caller, IMethod callee) { // check for the cache if (NEGATIVE_CACHE && hasNonCalls(context, caller) && getNonCalls(context, caller).contains(callee)) { Stats.INSTANCE.logMessage("Used negative cache!"); return false; } // in case we are searching for ourselves if (caller.equals(callee)) { return true; } // already processed if (processingMethods.contains(caller) || ! contexts.containsKey(context)) { if (NEGATIVE_CACHE) getMethodInfo(context, caller).addNonCall(callee); return false; } processingMethods.add(caller); // makes sure that all bindings are resolved for this caller in this context // this is needed because we must resolve bindings of overridden methods in a new context // (if they were called by super.m() for example) ContextInfo info = contexts.get(context); if (bindings.containsKey(caller) && ! info.hasMethodInfo(caller)) { callGraph.resolveBindings(context, caller); } // search for a regular call if (info.hasMethodInfo(caller) && info.getMethodInfo(caller).calls(callee)) { if (printMatchedCallsTrace) { callStack.add(caller); } return true; } // search for a static call if (staticCalls.containsKey(caller) && getStaticTargets(caller).contains(callee)) { if (printMatchedCallsTrace) { callStack.add(caller); } return true; } // must recursively search if (staticCalls.containsKey(caller) && iterateInternalCalls(getStaticTargets(caller), null, caller, callee)) return true; if (hasCalls(context, caller) && iterateInternalCalls(getCalls(context, caller), context, caller, callee)) return true; // learn it if (NEGATIVE_CACHE) info.getMethodInfo(caller).addNonCall(callee); return false; } /** * Helper method that iterates the internal calls query over the methods in the list, returning true if found. * * Note that dynamic calls are only the ones in the same hierarchy of the context. If there is a call to a method * outside the hierarchy, it MUST be a static call, i.e., it can be i) a static method call, ii) a constructor call * or iii) a polymorphic method call that was marked here as a static link to another method. * Therefore, if we are dealing with static calls, the context parameter must be NULL, and we iterate using the target * type as the new context. * * * @param list * @param context * @param caller * @param callee * @return */ protected boolean iterateInternalCalls(List<IMethod> list, IType context, IMethod caller, IMethod callee) { IType typeContext = context; for(IMethod target : list) { // if context is null, the context will be the target class if (context == null) { try { typeContext = JavaModelUtils.getDeclaringType(target); } catch (JavaModelException e) { e.printStackTrace(); continue; } } if (internalCalls(typeContext, target, callee)) { // also learn partials if (CACHE) getMethodInfo(context, caller).addCall(callee); return true; } } return false; } /** * Prints the stack of calls that make the path between this caller and the callee * * @param caller * @param callee */ protected void printStack(IMethod caller, IMethod callee) { // Remove the case where the only caller in the stack is this caller here if (callStack.size() == 1 && callStack.contains(caller)) { return; } // String Buffer are MUCH faster than "adding" strings StringBuffer buf = new StringBuffer(); buf.append("Trace from "); buf.append(JavaModelUtils.formatMethod(caller)); buf.append(" to "); buf.append(JavaModelUtils.formatMethod(callee)); buf.append("\n"); for(IMethod method : callStack) { if (! method.equals(caller)) { buf.append(" "); buf.append(JavaModelUtils.formatMethod(method)); buf.append("\n"); } } Stats.INSTANCE.logMessage(buf.toString()); } /** * Removes all information in this object */ public void clear() { contexts.clear(); bindings.clear(); processingMethods.clear(); staticCalls.clear(); } /** * Keeps method information for a certain context. * * @author Thiago Tonelli Bartolomei <ttonelli@gsd.uwaterloo.ca> */ protected class ContextInfo { /** * The methodInfos for a certain method */ protected Map<IMethod, MethodInfo> methodInfos = new HashMap<IMethod, MethodInfo>(); /** * Gets the method info for this method, creating if needed. * * If the intent is to check if there is method infor for a certain method, * it is more efficient to use hasMethodInfo instead * * @param method * @return */ public MethodInfo getMethodInfo(IMethod method) { if (! methodInfos.containsKey(method)) { methodInfos.put(method, new MethodInfo()); } return methodInfos.get(method); } /** * Checks if the method info for this method exists. * * @param method * @return */ public boolean hasMethodInfo(IMethod method) { return methodInfos.containsKey(method); } } /** * Keeps method call and non call information for a certain method. * * @author Thiago Tonelli Bartolomei <ttonelli@gsd.uwaterloo.ca> */ protected class MethodInfo { /** * The set of calls made by this method */ protected List<IMethod> calls = null; /** * The set of methods we know for sure this method does not call */ protected List<IMethod> nonCalls = null; /** * Adds a new call to the list of calls, creating the list if needed. * * @param call */ public void addCall(IMethod call) { if (calls == null) { calls = new ArrayList<IMethod>(5); } if (! calls.contains(call)) calls.add(call); } /** * Adds a new Non Call to the list of non calls, creating the list of needed * * @param nonCall */ public void addNonCall(IMethod nonCall) { if (nonCalls == null) { nonCalls = new ArrayList<IMethod>(5); } if (! nonCalls.contains(nonCall)) nonCalls.add(nonCall); } /** * Checks if this method calls this callee * * @param method * @return */ public boolean calls(IMethod callee) { return calls != null && calls.contains(callee); } /** * Checks if we know that this method does not call this callee * * @param callee * @return */ public boolean doesNotCall(IMethod callee) { return nonCalls != null && nonCalls.contains(callee); } /** * Checks if we have information about calls * * @return */ public boolean hasCalls() { return calls != null; } /** * Checks if we have information about non calls * * @return */ public boolean hasNonCalls() { return nonCalls != null; } /** * Gets the set of calls * * @return */ public List<IMethod> getCalls() { return calls; } /** * Gets the set of non calls * * @return */ public List<IMethod> getNonCalls() { return nonCalls; } } }