Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement call graph pruning feature #1025

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/

import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
Expand Down Expand Up @@ -59,6 +60,8 @@ public abstract class AbstractCallGraphAlgorithm implements CallGraphAlgorithm {

private static final Logger logger = LoggerFactory.getLogger(AbstractCallGraphAlgorithm.class);

private BiFunction<SootMethod, InvokableStmt, Boolean> boundFunction;

@Nonnull protected final View view;

protected AbstractCallGraphAlgorithm(@Nonnull View view) {
Expand Down Expand Up @@ -232,7 +235,7 @@ protected void resolveAllCallsFromSourceMethod(
.map(Stmt::asInvokableStmt)
.forEach(
stmt ->
resolveCall(sourceMethod, stmt)
(boundFunction == null || Boolean.TRUE.equals(boundFunction.apply(sourceMethod, stmt)) ? resolveCall(sourceMethod, stmt) : Stream.<MethodSignature> empty())
bingtimren marked this conversation as resolved.
Show resolved Hide resolved
.forEach(
targetMethod ->
addCallToCG(
Expand Down Expand Up @@ -586,4 +589,25 @@ public static Optional<SootMethod> findConcreteMethod(
+ " and in its superclasses and interfaces");
return Optional.empty();
}

/**
* @return the bound function, see @setBoundFunction
*/
public BiFunction<SootMethod, InvokableStmt, Boolean> getBoundFunction()
{
return boundFunction;
}

/** Set a bound function to prune the call graph. The bound function accepts the source method and
* an InvokableStatement, which is in the source method's body, and determines if the call from
* the source method to the invocation target shall be added into the call graph.
*
* This allows building an pruned & potentially incomplete call graph, with lower memory / CPU
* cost.
*
* @param boundFunction */
public void setBoundFunction(BiFunction<SootMethod, InvokableStmt, Boolean> boundFunction)
{
this.boundFunction = boundFunction;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets set the function in the constructor only

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm.... but this would force all sub-classes implement two constructors - one with a bound function, one without (so to use the always-true function)..... instead of having this feature automatically

let's keep it this way, or change setter to a "with" method (fluent API)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you are concerned bound function is set after constructCompleteCallGraph() is called, I can add a checker and throw an exception if user do that. thought?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets implement it as a method directly without BiPredicate etc. - that way someone can override it the same way its done with a linkedhashmap to transform it to a LRUcache via overriding removeoldestentry() - e.g. see https://ashcode.medium.com/lru-cache-using-linkedhashmap-java-85e78ee1b976

Copy link
Author

@bingtimren bingtimren Aug 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still recommend keeping it this way. This is more a setting of the analyser than an extension to it. In general I would try to avoid overriding a non-abstract method, it's kind of breaking encapsulation. In general composition is preferred than inheritance, see https://en.wikipedia.org/wiki/Composition_over_inheritance

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you configure it in the constructor, the developer might be more aware of the feature.

}
180 changes: 180 additions & 0 deletions sootup.tests/src/test/java/sootup/tests/CallGraphTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import sootup.callgraph.AbstractCallGraphAlgorithm;
Expand Down Expand Up @@ -49,6 +51,11 @@ private JavaView createViewForClassPath(String classPath) {
}

CallGraph loadCallGraph() {
return loadCallGraph(null);
}

CallGraph loadCallGraph(BiFunction<SootMethod, InvokableStmt, Boolean> boundFunction)
{
double version = Double.parseDouble(System.getProperty("java.specification.version"));
if (version > 1.8) {
fail("The rt.jar is not available after Java 8. You are using version " + version);
Expand All @@ -71,6 +78,8 @@ CallGraph loadCallGraph() {
assertNotNull(m, mainMethodSignature + " not found in classloader");

AbstractCallGraphAlgorithm algorithm = createAlgorithm(view);
algorithm.setBoundFunction(boundFunction);

CallGraph cg = algorithm.initialize(Collections.singletonList(mainMethodSignature));

assertNotNull(cg);
Expand Down Expand Up @@ -205,6 +214,68 @@ public void testRTA() {
methodInterfaceImplementationNotInstatiated,
getInvokableStmt(mainMethodSignature, methodInterface)));
}

@Test
public void testRTAWithNegativeBoundFunction()
{
algorithmName = "RTA";
CallGraph cg = loadCallGraph((fromMethod, statement) -> false);

MethodSignature methodAbstract =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("AbstractClass"),
"method",
"int",
Collections.emptyList());
MethodSignature methodMethodImplementedInstantiatedInSubClass =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("MethodImplementedInstantiatedInSubClass"),
"method",
"int",
Collections.emptyList());
MethodSignature methodSubClassMethodImplemented =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("SubClassMethodImplemented"),
"method",
"int",
Collections.emptyList());

assertFalse(
cg.containsCall(
mainMethodSignature,
methodMethodImplementedInstantiatedInSubClass,
getInvokableStmt(mainMethodSignature, methodAbstract)));
assertFalse(
cg.containsCall(
mainMethodSignature,
methodSubClassMethodImplemented,
getInvokableStmt(mainMethodSignature, methodAbstract)));

MethodSignature methodInterface =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("Interface"),
"defaultMethod",
"int",
Collections.emptyList());
MethodSignature methodInterfaceImplementation =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("InterfaceImplementation"),
"defaultMethod",
"int",
Collections.emptyList());


assertFalse(
cg.containsCall(
mainMethodSignature,
methodInterface,
getInvokableStmt(mainMethodSignature, methodInterface)));
assertFalse(
cg.containsCall(
mainMethodSignature,
methodInterfaceImplementation,
getInvokableStmt(mainMethodSignature, methodInterface)));
}

@Test
public void testCHA() {
Expand Down Expand Up @@ -315,6 +386,115 @@ public void testCHA() {
getInvokableStmt(mainMethodSignature, methodInterface)));
}

@Test
public void testCHAWithPositiveBoundFunction() {
algorithmName = "CHA";
CallGraph cg = loadCallGraph((method, statement)->true);

MethodSignature methodAbstract =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("AbstractClass"),
"method",
"int",
Collections.emptyList());
MethodSignature methodMethodImplemented =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("MethodImplemented"),
"method",
"int",
Collections.emptyList());
MethodSignature methodMethodImplementedInstantiatedInSubClass =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("MethodImplementedInstantiatedInSubClass"),
"method",
"int",
Collections.emptyList());
MethodSignature methodSubClassMethodImplemented =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("SubClassMethodImplemented"),
"method",
"int",
Collections.emptyList());
MethodSignature methodSubClassMethodNotImplemented =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("SubClassMethodNotImplemented"),
"method",
"int",
Collections.emptyList());

assertFalse(
cg.containsCall(
mainMethodSignature,
methodAbstract,
getInvokableStmt(mainMethodSignature, methodAbstract)));
assertTrue(
cg.containsCall(
mainMethodSignature,
methodMethodImplemented,
getInvokableStmt(mainMethodSignature, methodAbstract)));
assertTrue(
cg.containsCall(
mainMethodSignature,
methodMethodImplementedInstantiatedInSubClass,
getInvokableStmt(mainMethodSignature, methodAbstract)));
assertFalse(
cg.containsCall(
mainMethodSignature,
methodSubClassMethodNotImplemented,
getInvokableStmt(mainMethodSignature, methodAbstract)));
assertTrue(
cg.containsCall(
mainMethodSignature,
methodSubClassMethodImplemented,
getInvokableStmt(mainMethodSignature, methodAbstract)));

MethodSignature methodInterface =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("Interface"),
"defaultMethod",
"int",
Collections.emptyList());
MethodSignature methodInterfaceNoImplementation =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("InterfaceNoImplementation"),
"defaultMethod",
"int",
Collections.emptyList());
MethodSignature methodInterfaceImplementation =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("InterfaceImplementation"),
"defaultMethod",
"int",
Collections.emptyList());
MethodSignature methodInterfaceImplementationNotInstatiated =
identifierFactory.getMethodSignature(
identifierFactory.getClassType("InterfaceImplementationNotInstatiated"),
"defaultMethod",
"int",
Collections.emptyList());

assertTrue(
cg.containsCall(
mainMethodSignature,
methodInterface,
getInvokableStmt(mainMethodSignature, methodInterface)));
assertTrue(
cg.containsCall(
mainMethodSignature,
methodInterfaceImplementation,
getInvokableStmt(mainMethodSignature, methodInterface)));
assertFalse(
cg.containsCall(
mainMethodSignature,
methodInterfaceNoImplementation,
getInvokableStmt(mainMethodSignature, methodInterface)));
assertTrue(
cg.containsCall(
mainMethodSignature,
methodInterfaceImplementationNotInstatiated,
getInvokableStmt(mainMethodSignature, methodInterface)));
}

@Test
public void checkCallGraphDotExporter() {
algorithmName = "RTA";
Expand Down
Loading