All Downloads are FREE. Search and download functionalities are using the official Maven repository.

proguard.analysis.datastructure.callgraph.CallGraph Maven / Gradle / Ivy

Go to download

ProGuardCORE is a free library to read, analyze, modify, and write Java class files.

There is a newer version: 9.1.6
Show newest version
/*
 * ProGuardCORE -- library to process Java bytecode.
 *
 * Copyright (c) 2002-2021 Guardsquare NV
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package proguard.analysis.datastructure.callgraph;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import proguard.classfile.ClassPool;
import proguard.classfile.Clazz;
import proguard.classfile.MethodSignature;
import proguard.util.CallGraphWalker;

/**
 * Collection of all {@link Call}s in a program, optimized for retrieval of incoming and outgoing
 * edges for any method in constant time.
 *
 * @author Samuel Hopstock
 */
public class CallGraph {

  private static final transient Logger log = LogManager.getLogger(CallGraph.class);
  public final Map> incoming;
  public final Map> outgoing;

  /** If true, incoming edges are not explored further for known entry points. */
  private static final boolean STOP_AT_ENTRYPOINT = true;

  private final boolean concurrent;

  /** Create an empty call graph. */
  public CallGraph() {
    this(new HashMap<>(), new HashMap<>(), false);
  }

  protected CallGraph(
      Map> incoming,
      Map> outgoing,
      boolean concurrent) {
    this.incoming = incoming;
    this.outgoing = outgoing;
    this.concurrent = concurrent;
  }

  /**
   * Provides concurrency ready {@link CallGraph}, backed by {@link ConcurrentHashMap}s and by
   * {@link Collections#synchronizedSet(Set) synchronizedSet}s. Not needed without multithreading.
   */
  public static CallGraph concurrentCallGraph() {
    return new CallGraph(new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), true);
  }

  /**
   * Add a {@link Call} to this call graph.
   *
   * @param call The call to be added.
   */
  public void addCall(Call call) {
    if (!(call.caller.signature instanceof MethodSignature)) {
      log.warn("Location of call {} is not a method", call);
      return;
    }
    if (call.getTarget() == null) {
      log.warn("Target of call {} is null", call);
      return;
    }

    outgoing.computeIfAbsent((MethodSignature) call.caller.signature, e -> newCallSet()).add(call);

    incoming.computeIfAbsent(call.getTarget(), e -> newCallSet()).add(call);
  }

  private Set newCallSet() {
    return concurrent ? Collections.synchronizedSet(new LinkedHashSet<>()) : new LinkedHashSet<>();
  }

  /** Clear the call graph references. */
  public void clear() {
    incoming.clear();
    outgoing.clear();
  }

  /**
   * See {@link #reconstructCallGraph(ClassPool, MethodSignature, int, int)}
   *
   * @param programClassPool The current {@link ClassPool} of the program that can be used for
   *     mapping. class names to the actual {@link Clazz}.
   * @param start The {@link MethodSignature} of the method whose incoming call graph should be
   *     calculated.
   * @return A {@link Node} that represents the single call graph root, i.e. the start method.
   */
  public Node reconstructCallGraph(ClassPool programClassPool, MethodSignature start) {
    return CallGraphWalker.predecessorPathsAccept(
        this, start, n -> handleUntilEntryPoint(programClassPool, n, null));
  }

  /**
   * Calculate the incoming call graph for a method of interest, showing how it can be reached.
   *
   * 

We have an inverted tree structure like the following example: * *

{@code
   * onCreate() <-- predecessor -- proxy() <-- predecessor -- root()
   *                            onResume() <-- predecessor ----|  |
   *                        unusedMethod() <-- predecessor -------|
   *
   * }
* * Here, {@code root()} is the method whose call graph is to be calculated, and the graph now * shows that it can be reached from {@code onCreate()} via {@code proxy()}, and also directly * from {@code onResume()} or {@code unusedMethod()}. * *

Generally, we still can't be sure whether the top most methods (leaves in the tree) can be * reached themselves, if we don't find any incoming edges. But if these methods are {@link * EntryPoint}s of an Android app, they will most likely be called at some point in the app * lifecycle. * * @param programClassPool The current {@link ClassPool} of the program that can be used for * mapping. class names to the actual {@link Clazz}. * @param start The {@link MethodSignature} of the method whose incoming call graph should be * calculated. * @param maxDepth maximal depth of reconstructed {@link CallGraph} similar to {@link * CallGraphWalker#MAX_DEPTH_DEFAULT}. * @param maxWidth maximal width of reconstructed {@link CallGraph} similar to {@link * CallGraphWalker#MAX_WIDTH_DEFAULT}. * @return A {@link Node} that represents the single call graph root, i.e. the start method. */ public Node reconstructCallGraph( ClassPool programClassPool, MethodSignature start, int maxDepth, int maxWidth) { return CallGraphWalker.predecessorPathsAccept( this, start, n -> handleUntilEntryPoint(programClassPool, n, null), maxDepth, maxWidth); } /** * Extension of {@link #reconstructCallGraph(ClassPool, MethodSignature)} that also collects all * {@link EntryPoint}s found along the way. * * @param programClassPool The current {@link ClassPool} of the program that can be used for * mapping. * @param start The {@link MethodSignature} of the method whose incoming call graph should be * calculated. * @param entryPoints A set that will be filled with all {@link EntryPoint}s that are part of the * incoming call graph. * @return A {@link Node} that represents the single call graph root, i.e. the start method. */ public Node reconstructCallGraph( ClassPool programClassPool, MethodSignature start, Set entryPoints) { return CallGraphWalker.predecessorPathsAccept( this, start, n -> handleUntilEntryPoint(programClassPool, n, entryPoints)); } /** * Handler implementation for {@link CallGraphWalker#predecessorPathsAccept(CallGraph, * MethodSignature, Predicate)} that checks discovered paths if they have arrived at a known entry * point. * * @param programClassPool The current {@link ClassPool} of the program that can be used for * mapping class names to the actual {@link Clazz}. * @param curr The {@link Node} that represents the currently discovered call graph node and its * successors. * @param entryPoints a set containing the entrypoints seen on this path, will be filled during * the reconstruction of the callgraph. * @return true if we have arrived at an entry point, so that the {@link CallGraphWalker} stops * exploring this particular path. */ private boolean handleUntilEntryPoint( ClassPool programClassPool, Node curr, Set entryPoints) { // Get all classes that contain known entryPoints and are superclasses of the current one Clazz currClass = programClassPool.getClass(curr.signature.getClassName()); if (currClass == null) { log.warn("Could not find class {} in class pool", curr.signature.getClassName()); curr.isTruncated = true; return false; } Set entrypointSuperclassNames = EntryPoint.WELL_KNOWN_ENTRYPOINT_CLASSES.stream() .filter(e -> classExtendsOrEquals(currClass, e.replace('.', '/'))) .collect(Collectors.toSet()); // If we are in a method overriding any known entrypoint, that's a call graph leaf Optional matchingEntrypoint = EntryPoint.WELL_KNOWN_ENTRYPOINTS.stream() .filter( e -> entrypointSuperclassNames.contains(e.className) && e.methodName.equals(curr.signature.method)) .findFirst(); if (matchingEntrypoint.isPresent()) { curr.matchingEntrypoint = matchingEntrypoint.get(); if (entryPoints != null) { entryPoints.add( new EntryPoint( matchingEntrypoint.get().type, curr.signature.getClassName(), curr.signature.method)); } return !STOP_AT_ENTRYPOINT; } return true; } /** * Check if a {@link Clazz} either matches the provided class name or extends this provided class. * Both direct and transitive inheritance is allowed. * * @param currClass The {@link Clazz} that might be equal to or a subclass of the provided class * name. * @param className The potential super class name. * @return True if currClass is equal to className or is one of its subclasses. */ private boolean classExtendsOrEquals(Clazz currClass, String className) { if (Objects.equals(currClass.getSuperName(), className)) { return true; } if (currClass.getSuperClass() != null) { if (Objects.equals(currClass.getSuperClass().getName(), className)) { return true; } return classExtendsOrEquals(currClass.getSuperClass(), className); } return false; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy