Skip to content

Commit 9c0966a

Browse files
committed
Merge branch 'matthieu-vergne-fix/broken_call_graph_for_nested_lambdas'
2 parents 4589557 + ad203c1 commit 9c0966a

File tree

4 files changed

+189
-3
lines changed

4 files changed

+189
-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: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright (c) 2018 - Matthieu Vergne <matthieu.vergne@gmail.com>
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
*
11+
* * Redistributions in binary form must reproduce the above
12+
* copyright notice, this list of conditions and the following
13+
* disclaimer in the documentation and/or other materials provided
14+
* with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27+
*/
28+
29+
package gr.gousiosg.javacg.stat;
30+
31+
import java.util.HashMap;
32+
import java.util.Map;
33+
import java.util.regex.Matcher;
34+
import java.util.regex.Pattern;
35+
36+
import org.apache.bcel.classfile.Attribute;
37+
import org.apache.bcel.classfile.BootstrapMethod;
38+
import org.apache.bcel.classfile.BootstrapMethods;
39+
import org.apache.bcel.classfile.ConstantCP;
40+
import org.apache.bcel.classfile.ConstantMethodHandle;
41+
import org.apache.bcel.classfile.ConstantNameAndType;
42+
import org.apache.bcel.classfile.ConstantPool;
43+
import org.apache.bcel.classfile.ConstantUtf8;
44+
import org.apache.bcel.classfile.JavaClass;
45+
import org.apache.bcel.classfile.Method;
46+
47+
/**
48+
* {@link DynamicCallManager} provides facilities to retrieve information about
49+
* dynamic calls statically.
50+
* <p>
51+
* Most of the time, call relationships are explicit, which allows to properly
52+
* build the call graph statically. But in the case of dynamic linking, i.e.
53+
* <code>invokedynamic</code> instructions, this relationship might be unknown
54+
* until the code is actually executed. Indeed, bootstrap methods are used to
55+
* dynamically link the code at first call. One can read details about the
56+
* <a href=
57+
* "https://docs.oracle.com/javase/8/docs/technotes/guides/vm/multiple-language-support.html#invokedynamic"><code>invokedynamic</code>
58+
* instruction</a> to know more about this mechanism.
59+
* <p>
60+
* Nested lambdas are particularly subject to such absence of concrete caller,
61+
* which lead us to produce method names like <code>lambda$null$0</code>, which
62+
* breaks the call graph. This information can however be retrieved statically
63+
* through the code of the bootstrap method called.
64+
* <p>
65+
* In {@link #retrieveCalls(Method, JavaClass)}, we retrieve the (called,
66+
* caller) relationships by analyzing the code of the caller {@link Method}.
67+
* This information is then used in {@link #linkCalls(Method)} to rename the
68+
* called {@link Method} properly.
69+
*
70+
* @author Matthieu Vergne <matthieu.vergne@gmail.com>
71+
*/
72+
public class DynamicCallManager {
73+
private static final Pattern BOOTSTRAP_CALL_PATTERN = Pattern
74+
.compile("invokedynamic\t(\\d+):\\S+ \\S+ \\(\\d+\\)");
75+
private static final int CALL_HANDLE_INDEX_ARGUMENT = 1;
76+
77+
private final Map<String, String> dynamicCallers = new HashMap<>();
78+
79+
/**
80+
* Retrieve dynamic call relationships based on the code of the provided
81+
* {@link Method}.
82+
*
83+
* @param method {@link Method} to analyze the code
84+
* @param jc {@link JavaClass} info, which contains the bootstrap methods
85+
* @see #linkCalls(Method)
86+
*/
87+
public void retrieveCalls(Method method, JavaClass jc) {
88+
if (method.isAbstract()) {
89+
// No code to consider
90+
return;
91+
}
92+
ConstantPool cp = method.getConstantPool();
93+
BootstrapMethod[] boots = getBootstrapMethods(jc);
94+
String code = method.getCode().toString();
95+
Matcher matcher = BOOTSTRAP_CALL_PATTERN.matcher(code);
96+
while (matcher.find()) {
97+
int bootIndex = Integer.parseInt(matcher.group(1));
98+
BootstrapMethod bootMethod = boots[bootIndex];
99+
int calledIndex = bootMethod.getBootstrapArguments()[CALL_HANDLE_INDEX_ARGUMENT];
100+
String calledName = getMethodNameFromHandleIndex(cp, calledIndex);
101+
String callerName = method.getName();
102+
dynamicCallers.put(calledName, callerName);
103+
}
104+
}
105+
106+
private String getMethodNameFromHandleIndex(ConstantPool cp, int callIndex) {
107+
ConstantMethodHandle handle = (ConstantMethodHandle) cp.getConstant(callIndex);
108+
ConstantCP ref = (ConstantCP) cp.getConstant(handle.getReferenceIndex());
109+
ConstantNameAndType nameAndType = (ConstantNameAndType) cp.getConstant(ref.getNameAndTypeIndex());
110+
return nameAndType.getName(cp);
111+
}
112+
113+
/**
114+
* Link the {@link Method}'s name to its concrete caller if required.
115+
*
116+
* @param method {@link Method} to analyze
117+
* @see #retrieveCalls(Method, JavaClass)
118+
*/
119+
public void linkCalls(Method method) {
120+
int nameIndex = method.getNameIndex();
121+
ConstantPool cp = method.getConstantPool();
122+
String methodName = ((ConstantUtf8) cp.getConstant(nameIndex)).getBytes();
123+
String linkedName = methodName;
124+
String callerName = methodName;
125+
while (linkedName.matches("(lambda\\$)+null(\\$\\d+)+")) {
126+
callerName = dynamicCallers.get(callerName);
127+
linkedName = linkedName.replace("null", callerName);
128+
}
129+
cp.setConstant(nameIndex, new ConstantUtf8(linkedName));
130+
}
131+
132+
private BootstrapMethod[] getBootstrapMethods(JavaClass jc) {
133+
for (Attribute attribute : jc.getAttributes()) {
134+
if (attribute instanceof BootstrapMethods) {
135+
return ((BootstrapMethods) attribute).getBootstrapMethods();
136+
}
137+
}
138+
return new BootstrapMethod[]{};
139+
}
140+
}

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)