blob: 0f5924dc3edca4a6f783ffd8a6d212aff9196cca [file] [log] [blame]
// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.naming;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexCallSite;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.desugar.LambdaDescriptor;
import com.android.tools.r8.naming.MethodNameMinifier.State;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.DisjointSets;
import com.android.tools.r8.utils.MethodJavaSignatureEquivalence;
import com.android.tools.r8.utils.MethodSignatureEquivalence;
import com.android.tools.r8.utils.Timing;
import com.google.common.base.Equivalence;
import com.google.common.base.Equivalence.Wrapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Assigning names to interface methods can be done in different ways, but here we try to assign the
* same name to equivalent methods. Arguments for grouping equivalent methods is that clients will
* work out of the box if they implement multiple interfaces and the penalty of not having more
* locality is insignificant in DEX because the proto will only be listed once in the DEX file.
*
* ----------- Library -----------
*
* class A { }
*
* class Z extends A { a(); }
*
* ----------- Program -----------
*
* interface I { x(); c() }
*
* / \
* / \
* / \
* v v
*
* interface J { b() } interface K { d() } interface L { b() }
*
* B extends A implements J { }
*
* C extends B implements K { }
*
* -keep L { *; }
*
* Because of the way this algorithm work, we will try to bundle the naming together into groups. In
* the example above, the group states should identify that:
*
* - We are bundling J.b() with L.b() so we need to keep the name of both
* - When giving name to I.x() or I.c() we cannot use b() because those names would collide in C.
*
* A further complication is that call sites can implement methods with the same name but different
* proto's. The canonical example of this is the identity function. We compute all callsites that
* needs to be named together by using union-find.
*
* A small sample of the state of the above example could be like so:
* Group a() -> { State(A) }
* Group a(Object) -> { State(J), State(I), State(A) }
* Group b() -> { State(J), State(I), State(L), State(A) }
* Group c() -> { State(J), State(I), State(A) }
*
* Because of the frontier state computation in {@link MethodNameMinifier}, all reservations are
* bubbled up to the library frontier and naming is top-down to not re-use the same names. The
* {@link InterfaceMethodNameMinifier} is run after ordinary method reservation but before new
* method name assignment. Thus each group only has to keep track of the states in the interface
* inheritance tree and the frontiers of their implementations.
*
* To cache all interface reservation states we use interfaceStateMap that maps each type to its
* {@link InterfaceReservationState} that allows for querying and updating the interface inheritance
* tree. This caching is crucial for the time spent computing interface names because most states
* will not have a high depth.
*
* We then map each group from Equivalence(Method) to {@link InterfaceMethodGroupState} that
* maintains a collection of {@link InterfaceReservationState} for each method the group represent.
*/
class InterfaceMethodNameMinifier {
class InterfaceReservationState {
// Used for iterating the parent hierarchy tree.
final DexClass iface;
// Used for iterating the sub trees that has this node as root.
final Set<DexType> children = new HashSet<>();
// Collection of the frontier reservation types and the interface type itself.
private final Set<DexType> reservationTypes = new HashSet<>();
InterfaceReservationState(DexClass iface) {
this.iface = iface;
}
DexString getReservedName(DexEncodedMethod method) {
// If an interface is kept and we are using applymapping, the renamed name for this method
// is tracked on this level.
if (appView.options().getProguardConfiguration().hasApplyMappingFile()) {
DexString reservedName = minifierState.getReservedName(method, iface);
if (reservedName != null) {
return reservedName;
}
}
// Otherwise, we just search the hierarchy for the first identity reservation since
// applymapping is no longer in effect.
Boolean isReserved =
forAny(
s -> {
for (DexType reservationType : s.reservationTypes) {
Set<DexString> reservedNamesFor =
minifierState
.getReservationState(reservationType)
.getReservedNamesFor(method.getReference());
assert reservedNamesFor == null || !reservedNamesFor.isEmpty();
if (reservedNamesFor != null && reservedNamesFor.contains(method.getName())) {
return true;
}
}
return null;
});
return isReserved == null ? null : method.getName();
}
void addReservationType(DexType type) {
this.reservationTypes.add(type);
}
void reserveName(DexString reservedName, DexEncodedMethod method) {
forAll(
s -> {
s.reservationTypes.forEach(
resType -> {
MethodReservationState<?> state = minifierState.getReservationState(resType);
state.reserveName(reservedName, method);
});
});
}
boolean isAvailable(DexString candidate, DexEncodedMethod method) {
Boolean result =
forAny(
s -> {
for (DexType resType : s.reservationTypes) {
MethodNamingState<?> state = minifierState.getNamingState(resType);
if (!state.isAvailable(candidate, method.getReference())) {
return false;
}
}
return null;
});
return result == null || result;
}
void addRenaming(DexString newName, DexEncodedMethod method) {
forAll(
s ->
s.reservationTypes.forEach(
resType -> minifierState.getNamingState(resType).addRenaming(newName, method)));
}
<T> void forAll(Consumer<InterfaceReservationState> action) {
forAny(
s -> {
action.accept(s);
return null;
});
}
private <T> T forAny(Function<InterfaceReservationState, T> action) {
T result = action.apply(this);
if (result != null) {
return result;
}
result = forChildren(action);
if (result != null) {
return result;
}
return forParents(action);
}
private <T> T forParents(Function<InterfaceReservationState, T> action) {
for (DexType parent : iface.interfaces.values) {
InterfaceReservationState parentState = interfaceStateMap.get(parent);
if (parentState != null) {
T returnValue = action.apply(parentState);
if (returnValue != null) {
return returnValue;
}
returnValue = parentState.forParents(action);
if (returnValue != null) {
return returnValue;
}
}
}
return null;
}
private <T> T forChildren(Function<InterfaceReservationState, T> action) {
for (DexType child : children) {
InterfaceReservationState childState = interfaceStateMap.get(child);
if (childState != null) {
T returnValue = action.apply(childState);
if (returnValue != null) {
return returnValue;
}
returnValue = childState.forChildren(action);
if (returnValue != null) {
return returnValue;
}
}
}
return null;
}
boolean containsReservation(DexType reservationType) {
return reservationTypes.contains(reservationType);
}
}
class InterfaceMethodGroupState implements Comparable<InterfaceMethodGroupState> {
private final Set<DexCallSite> callSites = new HashSet<>();
private final Map<DexEncodedMethod, Set<InterfaceReservationState>> methodStates =
new HashMap<>();
private final List<DexEncodedMethod> callSiteCollidingMethods = new ArrayList<>();
void addState(DexEncodedMethod method, InterfaceReservationState interfaceState) {
methodStates.computeIfAbsent(method, m -> new HashSet<>()).add(interfaceState);
}
void appendMethodGroupState(InterfaceMethodGroupState state) {
callSites.addAll(state.callSites);
callSiteCollidingMethods.addAll(state.callSiteCollidingMethods);
for (DexEncodedMethod key : state.methodStates.keySet()) {
methodStates.computeIfAbsent(key, k -> new HashSet<>()).addAll(state.methodStates.get(key));
}
}
void addCallSite(DexCallSite callSite) {
// We cannot assert !callSites.contains(callSite) because the equivalence on methods
// may group different implementations to the same InterfaceMethodGroupState.
callSites.add(callSite);
}
DexString getReservedName() {
if (methodStates.isEmpty()) {
return null;
}
// It is perfectly fine to have multiple reserved names inside a group. If we have an identity
// reservation, we have to prioritize that over the others, otherwise we just propose the
// first ordered reserved name since we do not allow overwriting the name.
List<DexEncodedMethod> sortedMethods = Lists.newArrayList(methodStates.keySet());
sortedMethods.sort((x, y) -> x.getReference().compareTo(y.getReference()));
DexString reservedName = null;
for (DexEncodedMethod method : sortedMethods) {
for (InterfaceReservationState state : methodStates.get(method)) {
DexString stateReserved = state.getReservedName(method);
if (stateReserved == method.getName()) {
return method.getName();
} else if (stateReserved != null) {
reservedName = stateReserved;
}
}
}
return reservedName;
}
void reserveName(DexString reservedName) {
// The proposed reserved name is basically a suggestion. Try to reserve it in as many states
// as possible.
forEachState(
(method, state) -> {
DexString stateReserved = state.getReservedName(method);
if (stateReserved != null) {
state.reserveName(stateReserved, method);
minifierState.putRenaming(method, stateReserved);
} else {
state.reserveName(reservedName, method);
minifierState.putRenaming(method, reservedName);
}
});
}
boolean isAvailable(DexString candidate) {
Boolean result =
forAnyState(
(m, s) -> {
if (!s.isAvailable(candidate, m)) {
return false;
}
return null;
});
return result == null || result;
}
void addRenaming(DexString newName, MethodNameMinifier.State minifierState) {
forEachState(
(m, s) -> {
s.addRenaming(newName, m);
minifierState.putRenaming(m, newName);
});
}
void forEachState(BiConsumer<DexEncodedMethod, InterfaceReservationState> action) {
forAnyState(
(s, i) -> {
action.accept(s, i);
return null;
});
}
<T> T forAnyState(BiFunction<DexEncodedMethod, InterfaceReservationState, T> callback) {
T returnValue;
for (Map.Entry<DexEncodedMethod, Set<InterfaceReservationState>> entry :
methodStates.entrySet()) {
for (InterfaceReservationState state : entry.getValue()) {
returnValue = callback.apply(entry.getKey(), state);
if (returnValue != null) {
return returnValue;
}
}
}
return null;
}
boolean containsReservation(DexEncodedMethod method, DexType reservationType) {
Set<InterfaceReservationState> states = methodStates.get(method);
if (states != null) {
for (InterfaceReservationState state : states) {
if (state.containsReservation(reservationType)) {
return true;
}
}
}
return false;
}
@Override
public int compareTo(InterfaceMethodGroupState o) {
// Sort by most naming states to smallest.
return o.methodStates.size() - methodStates.size();
}
}
private final AppView<AppInfoWithLiveness> appView;
private final Equivalence<DexMethod> equivalence;
private final Equivalence<DexEncodedMethod> definitionEquivalence;
private final MethodNameMinifier.State minifierState;
/** A map from DexMethods to all the states linked to interfaces they appear in. */
private final Map<Wrapper<DexEncodedMethod>, InterfaceMethodGroupState> globalStateMap =
new HashMap<>();
/** A map for caching all interface states. */
private final Map<DexType, InterfaceReservationState> interfaceStateMap = new HashMap<>();
InterfaceMethodNameMinifier(AppView<AppInfoWithLiveness> appView, State minifierState) {
this.appView = appView;
this.minifierState = minifierState;
this.equivalence =
appView.options().getProguardConfiguration().isOverloadAggressively()
? MethodSignatureEquivalence.get()
: MethodJavaSignatureEquivalence.get();
this.definitionEquivalence =
new Equivalence<DexEncodedMethod>() {
@Override
protected boolean doEquivalent(DexEncodedMethod method, DexEncodedMethod other) {
return equivalence.equivalent(method.getReference(), other.getReference());
}
@Override
protected int doHash(DexEncodedMethod method) {
return equivalence.hash(method.getReference());
}
};
}
private Comparator<Wrapper<DexEncodedMethod>> getDefaultInterfaceMethodOrdering() {
return Comparator.comparing(globalStateMap::get);
}
private void reserveNamesInInterfaces(Iterable<DexClass> interfaces) {
for (DexClass iface : interfaces) {
assert iface.isInterface();
minifierState.allocateReservationStateAndReserve(iface.type, iface.type);
InterfaceReservationState iFaceState = new InterfaceReservationState(iface);
iFaceState.addReservationType(iface.type);
interfaceStateMap.put(iface.type, iFaceState);
}
}
void assignNamesToInterfaceMethods(Timing timing, Iterable<DexClass> interfaces) {
timing.begin("Interface minification");
// Reserve all the names that are required for interfaces.
timing.begin("Reserve direct and compute hierarchy");
reserveNamesInInterfaces(interfaces);
// Patch up root and children for all interfaces. Together with interfaceStateMap one can query
// and update the entire tree.
patchUpChildrenInReservationStates();
timing.end();
// Compute a map from method signatures to a set of naming states for interfaces and
// frontier states of classes that implement them. We add the frontier states so that we can
// reserve the names for later method naming.
timing.begin("Compute map");
computeReservationFrontiersForAllImplementingClasses();
for (DexClass iface : interfaces) {
InterfaceReservationState inheritanceState = interfaceStateMap.get(iface.type);
assert inheritanceState != null;
for (DexEncodedMethod method : iface.methods()) {
Wrapper<DexEncodedMethod> key = definitionEquivalence.wrap(method);
globalStateMap
.computeIfAbsent(key, k -> new InterfaceMethodGroupState())
.addState(method, inheritanceState);
}
}
timing.end();
// Collect the live call sites for multi-interface lambda expression renaming. For code with
// desugared lambdas this is a conservative estimate, as we don't track if the generated
// lambda classes survive into the output. As multi-interface lambda expressions are rare
// this is not a big deal.
Set<DexCallSite> liveCallSites = appView.appInfo().callSites.keySet();
// If the input program contains a multi-interface lambda expression that implements
// interface methods with different protos, we need to make sure tha the implemented lambda
// methods are renamed to the same name.
// Union-find structure to keep track of methods that must be renamed together.
// Note that if the input does not use multi-interface lambdas unificationParent will remain
// empty.
timing.begin("Union-find");
DisjointSets<Wrapper<DexEncodedMethod>> unification = new DisjointSets<>();
liveCallSites.forEach(
callSite -> {
Set<Wrapper<DexEncodedMethod>> callSiteMethods = new HashSet<>();
// Don't report errors, as the set of call sites is a conservative estimate, and can
// refer to interfaces which has been removed.
Set<DexEncodedMethod> implementedMethods =
appView.appInfo().lookupLambdaImplementedMethods(callSite);
if (implementedMethods.isEmpty()) {
return;
}
for (DexEncodedMethod method : implementedMethods) {
Wrapper<DexEncodedMethod> wrapped = definitionEquivalence.wrap(method);
InterfaceMethodGroupState groupState = globalStateMap.get(wrapped);
assert groupState != null : wrapped;
groupState.addCallSite(callSite);
callSiteMethods.add(wrapped);
}
if (callSiteMethods.isEmpty()) {
return;
}
// For intersection types, we have to iterate all the multiple interfaces to look for
// methods with the same signature.
List<DexType> implementedInterfaces =
LambdaDescriptor.getInterfaces(callSite, appView.appInfo());
if (implementedInterfaces != null) {
for (int i = 1; i < implementedInterfaces.size(); i++) {
// Add the merging state for all additional implemented interfaces into the state
// for the group, if the name is different, to ensure that we do not pick the same
// name.
DexClass iface = appView.definitionFor(implementedInterfaces.get(i));
assert iface.isInterface();
for (DexEncodedMethod implementedMethod : implementedMethods) {
for (DexEncodedMethod virtualMethod : iface.virtualMethods()) {
boolean differentName = implementedMethod.getName() != virtualMethod.getName();
if (differentName
&& MethodJavaSignatureEquivalence.getEquivalenceIgnoreName()
.equivalent(implementedMethod.method, virtualMethod.method)) {
InterfaceMethodGroupState interfaceMethodGroupState =
globalStateMap.computeIfAbsent(
definitionEquivalence.wrap(implementedMethod),
k -> new InterfaceMethodGroupState());
interfaceMethodGroupState.callSiteCollidingMethods.add(virtualMethod);
}
}
}
}
}
if (callSiteMethods.size() > 1) {
// Implemented interfaces have different protos. Unify them.
Wrapper<DexEncodedMethod> mainKey = callSiteMethods.iterator().next();
Wrapper<DexEncodedMethod> representative = unification.findOrMakeSet(mainKey);
for (Wrapper<DexEncodedMethod> key : callSiteMethods) {
unification.unionWithMakeSet(representative, key);
}
}
});
timing.end();
// We now have roots for all unions. Add all of the states for the groups to the method state
// for the unions to allow consistent naming across different protos.
timing.begin("States for union");
Map<Wrapper<DexEncodedMethod>, Set<Wrapper<DexEncodedMethod>>> unions =
unification.collectSets();
for (Wrapper<DexEncodedMethod> wrapped : unions.keySet()) {
InterfaceMethodGroupState groupState = globalStateMap.get(wrapped);
assert groupState != null;
for (Wrapper<DexEncodedMethod> groupedMethod : unions.get(wrapped)) {
DexEncodedMethod method = groupedMethod.get();
assert method != null;
groupState.appendMethodGroupState(globalStateMap.get(groupedMethod));
}
}
timing.end();
timing.begin("Sort");
// Filter out the groups that is included both in the unification and in the map. We sort the
// methods by the number of dependent states, so that we use short names for method that are
// referenced in many places.
List<Wrapper<DexEncodedMethod>> interfaceMethodGroups =
globalStateMap.keySet().stream()
.filter(unification::isRepresentativeOrNotPresent)
.sorted(
appView
.options()
.testing
.minifier
.getInterfaceMethodOrderingOrDefault(getDefaultInterfaceMethodOrdering()))
.collect(Collectors.toList());
timing.end();
assert verifyAllMethodsAreRepresentedIn(interfaceMethodGroups);
assert verifyAllCallSitesAreRepresentedIn(interfaceMethodGroups);
timing.begin("Reserve in groups");
// It is important that this entire phase is run before given new names, to ensure all
// reservations are propagated to all naming states.
List<Wrapper<DexEncodedMethod>> nonReservedMethodGroups = new ArrayList<>();
for (Wrapper<DexEncodedMethod> interfaceMethodGroup : interfaceMethodGroups) {
InterfaceMethodGroupState groupState = globalStateMap.get(interfaceMethodGroup);
assert groupState != null;
DexString reservedName = groupState.getReservedName();
if (reservedName == null) {
nonReservedMethodGroups.add(interfaceMethodGroup);
} else {
// Propagate reserved name to all states.
groupState.reserveName(reservedName);
}
}
timing.end();
timing.begin("Rename in groups");
for (Wrapper<DexEncodedMethod> interfaceMethodGroup : nonReservedMethodGroups) {
InterfaceMethodGroupState groupState = globalStateMap.get(interfaceMethodGroup);
assert groupState != null;
assert groupState.getReservedName() == null;
DexString newName = assignNewName(interfaceMethodGroup.get(), groupState);
assert newName != null;
Set<String> loggingFilter = appView.options().extensiveInterfaceMethodMinifierLoggingFilter;
if (!loggingFilter.isEmpty()) {
Set<DexEncodedMethod> sourceMethods = groupState.methodStates.keySet();
if (sourceMethods.stream()
.map(DexEncodedMethod::toSourceString)
.anyMatch(loggingFilter::contains)) {
print(interfaceMethodGroup.get().getReference(), sourceMethods, System.out);
}
}
}
// After all naming is completed for callsites, we must ensure to rename all interface methods
// that can collide with the callsite method name.
for (Wrapper<DexEncodedMethod> interfaceMethodGroup : nonReservedMethodGroups) {
InterfaceMethodGroupState groupState = globalStateMap.get(interfaceMethodGroup);
if (groupState.callSiteCollidingMethods.isEmpty()) {
continue;
}
DexEncodedMethod key = interfaceMethodGroup.get();
MethodNamingState<?> keyNamingState = minifierState.getNamingState(key.getHolderType());
DexString existingRenaming = keyNamingState.newOrReservedNameFor(key);
assert existingRenaming != null;
for (DexEncodedMethod collidingMethod : groupState.callSiteCollidingMethods) {
DexString newNameInGroup = newNameInGroup(collidingMethod, keyNamingState, groupState);
minifierState.putRenaming(collidingMethod, newNameInGroup);
MethodNamingState<?> methodNamingState =
minifierState.getNamingState(collidingMethod.getReference().holder);
methodNamingState.addRenaming(newNameInGroup, collidingMethod);
keyNamingState.addRenaming(newNameInGroup, collidingMethod);
}
}
timing.end();
timing.end(); // end compute timing
}
private DexString assignNewName(DexEncodedMethod method, InterfaceMethodGroupState groupState) {
assert groupState.getReservedName() == null;
assert groupState.methodStates.containsKey(method);
assert groupState.containsReservation(method, method.getHolderType());
MethodNamingState<?> namingState = minifierState.getNamingState(method.getHolderType());
// Check if the name is available in all states.
DexString newName =
namingState.newOrReservedNameFor(
method, (candidate, ignore) -> groupState.isAvailable(candidate));
groupState.addRenaming(newName, minifierState);
return newName;
}
private DexString newNameInGroup(
DexEncodedMethod method,
MethodNamingState<?> namingState,
InterfaceMethodGroupState groupState) {
// Check if the name is available in all states.
return namingState.nextName(method, (candidate, ignore) -> groupState.isAvailable(candidate));
}
private void patchUpChildrenInReservationStates() {
for (Map.Entry<DexType, InterfaceReservationState> entry : interfaceStateMap.entrySet()) {
for (DexType parent : entry.getValue().iface.interfaces.values) {
InterfaceReservationState parentState = interfaceStateMap.get(parent);
if (parentState != null) {
parentState.children.add(entry.getKey());
}
}
}
}
private void computeReservationFrontiersForAllImplementingClasses() {
appView
.appInfo()
.forEachTypeInHierarchyOfLiveProgramClasses(
clazz -> {
// TODO(b/133091438): Extend the if check to test for !clazz.isLibrary().
if (!clazz.isInterface()) {
appView
.appInfo()
.implementedInterfaces(clazz.type)
.forEach(
(directlyImplemented, ignoreIsKnownAndReserveInAllCases) -> {
InterfaceReservationState iState =
interfaceStateMap.get(directlyImplemented);
if (iState != null) {
DexType frontierType = minifierState.getFrontier(clazz.type);
iState.addReservationType(frontierType);
// The reservation state should already be added, but if a class is
// extending
// an interface, we will not visit the class during the sub-type
// traversel
if (minifierState.getReservationState(clazz.type) == null) {
minifierState.allocateReservationStateAndReserve(
clazz.type, frontierType);
}
}
});
}
});
}
private boolean verifyAllCallSitesAreRepresentedIn(List<Wrapper<DexEncodedMethod>> groups) {
Set<Wrapper<DexEncodedMethod>> unifiedMethods = new HashSet<>(groups);
Set<DexCallSite> unifiedSeen = new HashSet<>();
Set<DexCallSite> seen = new HashSet<>();
for (Map.Entry<Wrapper<DexEncodedMethod>, InterfaceMethodGroupState> state :
globalStateMap.entrySet()) {
for (DexCallSite callSite : state.getValue().callSites) {
seen.add(callSite);
if (unifiedMethods.contains(state.getKey())) {
boolean added = unifiedSeen.add(callSite);
assert added;
}
}
}
assert seen.size() == unifiedSeen.size();
assert unifiedSeen.containsAll(seen);
return true;
}
private boolean verifyAllMethodsAreRepresentedIn(List<Wrapper<DexEncodedMethod>> groups) {
Set<Wrapper<DexEncodedMethod>> unifiedMethods = new HashSet<>(groups);
Set<DexEncodedMethod> unifiedSeen = Sets.newIdentityHashSet();
Set<DexEncodedMethod> seen = Sets.newIdentityHashSet();
for (Map.Entry<Wrapper<DexEncodedMethod>, InterfaceMethodGroupState> state :
globalStateMap.entrySet()) {
for (DexEncodedMethod method : state.getValue().methodStates.keySet()) {
seen.add(method);
if (unifiedMethods.contains(state.getKey())) {
boolean added = unifiedSeen.add(method);
assert added;
}
}
}
assert seen.size() == unifiedSeen.size();
assert unifiedSeen.containsAll(seen);
return true;
}
private void print(DexMethod method, Set<DexEncodedMethod> sourceMethods, PrintStream out) {
out.println("-----------------------------------------------------------------------");
out.println("assignNameToInterfaceMethod(`" + method.toSourceString() + "`)");
out.println("-----------------------------------------------------------------------");
out.println("Source methods:");
for (DexEncodedMethod sourceMethod : sourceMethods) {
out.println(" " + sourceMethod.toSourceString());
}
out.println("States:");
out.println();
}
}