Skip to content

Commit 48a5bf0

Browse files
committed
Merge branch 'fix/broken_call_graph_for_nested_lambdas' of https://github.com/matthieu-vergne/java-callgraph into matthieu-vergne-fix/broken_call_graph_for_nested_lambdas
2 parents 4589557 + e84db11 commit 48a5bf0

File tree

4 files changed

+165
-3
lines changed

4 files changed

+165
-3
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<dependency>
1818
<groupId>org.apache.bcel</groupId>
1919
<artifactId>bcel</artifactId>
20-
<version>6.0</version>
20+
<version>6.2</version>
2121
<scope>provided</scope>
2222
</dependency>
2323
<dependency>

src/main/java/gr/gousiosg/javacg/stat/ClassVisitor.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class ClassVisitor extends EmptyVisitor {
4545
private JavaClass clazz;
4646
private ConstantPoolGen constants;
4747
private String classReferenceFormat;
48+
private final DynamicCallManager DCManager = new DynamicCallManager();
4849

4950
public ClassVisitor(JavaClass jc) {
5051
clazz = jc;
@@ -55,8 +56,13 @@ public ClassVisitor(JavaClass jc) {
5556
public void visitJavaClass(JavaClass jc) {
5657
jc.getConstantPool().accept(this);
5758
Method[] methods = jc.getMethods();
58-
for (int i = 0; i < methods.length; i++)
59-
methods[i].accept(this);
59+
for (int i = 0; i < methods.length; i++) {
60+
Method method = methods[i];
61+
DCManager.retrieveCalls(method, jc);
62+
DCManager.linkCalls(method);
63+
method.accept(this);
64+
65+
}
6066
}
6167

6268
public void visitConstantPool(ConstantPool constantPool) {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package gr.gousiosg.javacg.stat;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
7+
8+
import org.apache.bcel.classfile.Attribute;
9+
import org.apache.bcel.classfile.BootstrapMethod;
10+
import org.apache.bcel.classfile.BootstrapMethods;
11+
import org.apache.bcel.classfile.ConstantCP;
12+
import org.apache.bcel.classfile.ConstantMethodHandle;
13+
import org.apache.bcel.classfile.ConstantNameAndType;
14+
import org.apache.bcel.classfile.ConstantPool;
15+
import org.apache.bcel.classfile.ConstantUtf8;
16+
import org.apache.bcel.classfile.JavaClass;
17+
import org.apache.bcel.classfile.Method;
18+
19+
/**
20+
* {@link DynamicCallManager} provides facilities to retrieve information about
21+
* dynamic calls statically.
22+
* <p>
23+
* Most of the time, call relationships are explicit, which allows to properly
24+
* build the call graph statically. But in the case of dynamic linking, i.e.
25+
* <code>invokedynamic</code> instructions, this relationship might be unknown
26+
* until the code is actually executed. Indeed, bootstrap methods are used to
27+
* dynamically link the code at first call. One can read details about the
28+
* <a href=
29+
* "https://docs.oracle.com/javase/8/docs/technotes/guides/vm/multiple-language-support.html#invokedynamic"><code>invokedynamic</code>
30+
* instruction</a> to know more about this mechanism.
31+
* <p>
32+
* Nested lambdas are particularly subject to such absence of concrete caller,
33+
* which lead us to produce method names like <code>lambda$null$0</code>, which
34+
* breaks the call graph. This information can however be retrieved statically
35+
* through the code of the bootstrap method called.
36+
* <p>
37+
* In {@link #retrieveCalls(Method, JavaClass)}, we retrieve the (called,
38+
* caller) relationships by analyzing the code of the caller {@link Method}.
39+
* This information is then used in {@link #linkCalls(Method)} to rename the
40+
* called {@link Method} properly.
41+
*
42+
* @author Matthieu Vergne <matthieu.vergne@gmail.com>
43+
*
44+
*/
45+
public class DynamicCallManager {
46+
private static final Pattern BOOTSTRAP_CALL_PATTERN = Pattern
47+
.compile("invokedynamic\t(\\d+):\\S+ \\S+ \\(\\d+\\)");
48+
private static final int CALL_HANDLE_INDEX_ARGUMENT = 1;
49+
50+
private final Map<String, String> dynamicCallers = new HashMap<>();
51+
52+
/**
53+
* Retrieve dynamic call relationships based on the code of the provided
54+
* {@link Method}.
55+
*
56+
* @param method
57+
* {@link Method} to analyze the code
58+
* @param jc
59+
* {@link JavaClass} info, which contains the bootstrap methods
60+
* @see #linkCalls(Method)
61+
*/
62+
public void retrieveCalls(Method method, JavaClass jc) {
63+
if (method.isAbstract()) {
64+
// No code to consider
65+
return;
66+
}
67+
ConstantPool cp = method.getConstantPool();
68+
BootstrapMethod[] boots = getBootstrapMethods(jc);
69+
String code = method.getCode().toString();
70+
Matcher matcher = BOOTSTRAP_CALL_PATTERN.matcher(code);
71+
while (matcher.find()) {
72+
int bootIndex = Integer.parseInt(matcher.group(1));
73+
BootstrapMethod bootMethod = boots[bootIndex];
74+
int calledIndex = bootMethod.getBootstrapArguments()[CALL_HANDLE_INDEX_ARGUMENT];
75+
String calledName = getMethodNameFromHandleIndex(cp, calledIndex);
76+
String callerName = method.getName();
77+
dynamicCallers.put(calledName, callerName);
78+
}
79+
}
80+
81+
private String getMethodNameFromHandleIndex(ConstantPool cp, int callIndex) {
82+
ConstantMethodHandle handle = (ConstantMethodHandle) cp.getConstant(callIndex);
83+
ConstantCP ref = (ConstantCP) cp.getConstant(handle.getReferenceIndex());
84+
ConstantNameAndType nameAndType = (ConstantNameAndType) cp.getConstant(ref.getNameAndTypeIndex());
85+
return nameAndType.getName(cp);
86+
}
87+
88+
/**
89+
* Link the {@link Method}'s name to its concrete caller if required.
90+
*
91+
* @param method
92+
* {@link Method} to analyze
93+
* @see #retrieveCalls(Method, JavaClass)
94+
*/
95+
public void linkCalls(Method method) {
96+
int nameIndex = method.getNameIndex();
97+
ConstantPool cp = method.getConstantPool();
98+
String methodName = ((ConstantUtf8) cp.getConstant(nameIndex)).getBytes();
99+
String linkedName = methodName;
100+
String callerName = methodName;
101+
while (linkedName.matches("(lambda\\$)+null(\\$\\d+)+")) {
102+
callerName = dynamicCallers.get(callerName);
103+
linkedName = linkedName.replace("null", callerName);
104+
}
105+
cp.setConstant(nameIndex, new ConstantUtf8(linkedName));
106+
}
107+
108+
private BootstrapMethod[] getBootstrapMethods(JavaClass jc) {
109+
for (Attribute attribute : jc.getAttributes()) {
110+
if (attribute instanceof BootstrapMethods) {
111+
return ((BootstrapMethods) attribute).getBootstrapMethods();
112+
}
113+
}
114+
return new BootstrapMethod[] {};
115+
}
116+
}

src/test/resources/gr/gousiosg/javacg/lambda.feature

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,43 @@ Feature: Lambda
3535
"""
3636
M:LambdaTest:lambda$methodA$0() (M)LambdaTest:methodB()
3737
"""
38+
39+
Scenario: Retrieve nested lambdas
40+
Given I have the class "NestedLambdaTest" with code:
41+
"""
42+
public class NestedLambdaTest {
43+
public void methodA() {
44+
Runner r = () -> {
45+
Runner r2 = () -> {
46+
Runner r3 = () -> methodB();
47+
r3.run();
48+
};
49+
r2.run();
50+
};
51+
r.run();
52+
}
53+
54+
public void methodB() {}
55+
}
56+
"""
57+
When I run the analyze
58+
# Creation of r in methodA
59+
Then the result should contain:
60+
"""
61+
M:NestedLambdaTest:methodA() (D)Runner:run(NestedLambdaTest)
62+
"""
63+
# Creation of r2 in r
64+
And the result should contain:
65+
"""
66+
M:NestedLambdaTest:lambda$methodA$2() (D)Runner:run(NestedLambdaTest)
67+
"""
68+
# Creation of r3 in r2
69+
And the result should contain:
70+
"""
71+
M:NestedLambdaTest:lambda$lambda$methodA$2$1() (D)Runner:run(NestedLambdaTest)
72+
"""
73+
# Call of methodB in r3
74+
And the result should contain:
75+
"""
76+
M:NestedLambdaTest:lambda$lambda$lambda$methodA$2$1$0() (M)NestedLambdaTest:methodB()
77+
"""

0 commit comments

Comments
 (0)