Skip to content

Commit 404f2ef

Browse files
author
AgeloVito
committed
Bump version to 4.2.5.1, upgrade dependencies
- Upgrade transformer-api 4.2.4→4.2.5, sqlite-jdbc 3.53.0.0→3.53.1.0, dex-tools 2.4.35→2.4.36 - Across-archive CHA virtual call resolution (outgoing + incoming) - Human-readable method signatures in JSON and tree output - Tree-formatted text output (├──/└──) for call chains - Independent callers/callees node budgets - AtomicInteger → plain int[] in graph traversal tools - Fix eligible neighbor list overflow bug
1 parent 5884bb4 commit 404f2ef

7 files changed

Lines changed: 191 additions & 41 deletions

File tree

pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<groupId>io.github.agelovito</groupId>
99
<artifactId>jd-mcp-duo</artifactId>
10-
<version>4.2.4.1</version>
10+
<version>4.2.5.1</version>
1111
<packaging>jar</packaging>
1212

1313
<name>JD MCP Duo</name>
@@ -30,7 +30,7 @@
3030
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
3131
<gson.version>2.14.0</gson.version>
3232
<slf4j.version>2.0.17</slf4j.version>
33-
<transformer-api.version>4.2.4</transformer-api.version>
33+
<transformer-api.version>4.2.5</transformer-api.version>
3434
</properties>
3535

3636

@@ -81,12 +81,12 @@
8181
<dependency>
8282
<groupId>org.xerial</groupId>
8383
<artifactId>sqlite-jdbc</artifactId>
84-
<version>3.53.0.0</version>
84+
<version>3.53.1.0</version>
8585
</dependency>
8686
<dependency>
8787
<groupId>de.femtopedia.dex2jar</groupId>
8888
<artifactId>dex-tools</artifactId>
89-
<version>2.4.35</version>
89+
<version>2.4.36</version>
9090
</dependency>
9191

9292
<!-- Test dependencies -->

src/main/java/sqlite/PersistentScopeIndex.java

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,68 @@ WHERE archive_path IN (%s) AND target_owner = ? AND target_name = ? AND target_d
287287
new MethodRef(rs.getString("source_owner"), rs.getString("source_name"), rs.getString("source_desc"))
288288
));
289289
}
290+
results.addAll(expandVirtualCallers(connection, target));
290291
return results;
291292
}
292293
}
293294
}
294295

296+
private Set<ScopedMethodRef> expandVirtualCallers(Connection connection, MethodRef target) throws Exception {
297+
String parentSql = """
298+
SELECT DISTINCT parent
299+
FROM (
300+
SELECT i.interface_name AS parent
301+
FROM interfaces i
302+
WHERE i.archive_path IN (%1$s) AND i.owner = ?
303+
UNION
304+
SELECT c.super_name AS parent
305+
FROM classes c
306+
WHERE c.archive_path IN (%1$s) AND c.internal_name = ? AND c.super_name IS NOT NULL
307+
)
308+
WHERE parent IS NOT NULL
309+
""".formatted(placeholders(scopePaths.size()));
310+
Set<String> parentOwners = new LinkedHashSet<>();
311+
try (PreparedStatement statement = connection.prepareStatement(parentSql)) {
312+
int index = bindPaths(statement, 1);
313+
statement.setString(index++, target.owner());
314+
statement.setString(index, target.owner());
315+
try (ResultSet rs = statement.executeQuery()) {
316+
while (rs.next()) {
317+
parentOwners.add(rs.getString("parent"));
318+
}
319+
}
320+
}
321+
322+
if (parentOwners.isEmpty()) {
323+
return Set.of();
324+
}
325+
326+
Set<ScopedMethodRef> results = new LinkedHashSet<>();
327+
String callerSql = """
328+
SELECT archive_path, source_owner, source_name, source_desc
329+
FROM calls
330+
WHERE archive_path IN (%s) AND target_owner = ? AND target_name = ? AND target_desc = ?
331+
ORDER BY archive_path, source_owner, source_name, source_desc
332+
""".formatted(placeholders(scopePaths.size()));
333+
try (PreparedStatement statement = connection.prepareStatement(callerSql)) {
334+
for (String parent : parentOwners) {
335+
int index = bindPaths(statement, 1);
336+
statement.setString(index++, parent);
337+
statement.setString(index++, target.name());
338+
statement.setString(index, target.descriptor());
339+
try (ResultSet rs = statement.executeQuery()) {
340+
while (rs.next()) {
341+
results.add(new ScopedMethodRef(
342+
Path.of(rs.getString("archive_path")),
343+
new MethodRef(rs.getString("source_owner"), rs.getString("source_name"), rs.getString("source_desc"))
344+
));
345+
}
346+
}
347+
}
348+
}
349+
return results;
350+
}
351+
295352
public Set<ScopedMethodRef> outgoingScoped(MethodRef source) throws Exception {
296353
String sql = """
297354
SELECT archive_path, target_owner, target_name, target_desc
@@ -305,12 +362,45 @@ WHERE archive_path IN (%s) AND source_owner = ? AND source_name = ? AND source_d
305362
statement.setString(index++, source.owner());
306363
statement.setString(index++, source.name());
307364
statement.setString(index, source.descriptor());
365+
try (ResultSet rs = statement.executeQuery()) {
366+
Set<ScopedMethodRef> results = new LinkedHashSet<>();
367+
while (rs.next()) {
368+
MethodRef callee = new MethodRef(rs.getString("target_owner"), rs.getString("target_name"), rs.getString("target_desc"));
369+
results.add(new ScopedMethodRef(Path.of(rs.getString("archive_path")), callee));
370+
results.addAll(expandVirtualTargets(connection, callee));
371+
}
372+
return results;
373+
}
374+
}
375+
}
376+
377+
private Set<ScopedMethodRef> expandVirtualTargets(Connection connection, MethodRef target) throws Exception {
378+
String sql = """
379+
SELECT m.archive_path, m.owner, m.name, m.descriptor
380+
FROM methods m
381+
WHERE m.archive_path IN (%s)
382+
AND m.name = ? AND m.descriptor = ?
383+
AND m.owner IN (
384+
SELECT DISTINCT c.internal_name
385+
FROM classes c
386+
LEFT JOIN interfaces i ON i.archive_path = c.archive_path AND i.owner = c.internal_name
387+
WHERE c.archive_path IN (%1$s)
388+
AND (c.super_name = ? OR i.interface_name = ?)
389+
)
390+
ORDER BY m.archive_path, m.owner
391+
""".formatted(placeholders(scopePaths.size()));
392+
try (PreparedStatement statement = connection.prepareStatement(sql)) {
393+
int index = bindPaths(statement, 1);
394+
statement.setString(index++, target.name());
395+
statement.setString(index++, target.descriptor());
396+
statement.setString(index++, target.owner());
397+
statement.setString(index, target.owner());
308398
try (ResultSet rs = statement.executeQuery()) {
309399
Set<ScopedMethodRef> results = new LinkedHashSet<>();
310400
while (rs.next()) {
311401
results.add(new ScopedMethodRef(
312402
Path.of(rs.getString("archive_path")),
313-
new MethodRef(rs.getString("target_owner"), rs.getString("target_name"), rs.getString("target_desc"))
403+
new MethodRef(rs.getString("owner"), rs.getString("name"), rs.getString("descriptor"))
314404
));
315405
}
316406
return results;

src/main/java/support/GraphSupport.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.google.gson.JsonObject;
55

66
import java.util.Set;
7-
import java.util.concurrent.atomic.AtomicInteger;
87

98
public final class GraphSupport {
109
private GraphSupport() {
@@ -19,13 +18,13 @@ public static JsonObject graphMeta(String direction, int depth, int maxNodes) {
1918
return json;
2019
}
2120

22-
public static boolean consumeNode(AtomicInteger remaining, JsonObject rootMeta, JsonObject node) {
23-
if (remaining.get() <= 0) {
21+
public static boolean consumeNode(int[] remaining, JsonObject rootMeta, JsonObject node) {
22+
if (remaining[0] <= 0) {
2423
node.addProperty("truncated", true);
2524
rootMeta.addProperty("truncated", true);
2625
return false;
2726
}
28-
remaining.decrementAndGet();
27+
remaining[0]--;
2928
return true;
3029
}
3130

src/main/java/tools/CallChainTool.java

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.util.Deque;
1919
import java.util.HashSet;
2020
import java.util.Set;
21-
import java.util.concurrent.atomic.AtomicInteger;
2221

2322
public class CallChainTool implements MCPTool {
2423
@Override
@@ -60,6 +59,8 @@ public ToolResult execute(JsonObject arguments) throws Exception {
6059
String direction = normalizeDirection(JsonUtils.getString(arguments, "direction", "both"));
6160
int depth = JsonUtils.getInt(arguments, "depth", 3);
6261
int maxNodes = JsonUtils.getInt(arguments, "maxNodes", 128);
62+
if (depth < 1) depth = 1;
63+
if (maxNodes < 1) maxNodes = 1;
6364

6465
var candidates = index.resolveMethodCandidates(owner, methodName, descriptor);
6566
var broadCandidates = (descriptor != null && !descriptor.isBlank()) ? index.resolveMethodCandidates(owner, methodName, null) : candidates;
@@ -74,7 +75,6 @@ public ToolResult execute(JsonObject arguments) throws Exception {
7475
}
7576

7677
ScopedMethodRef root = new ScopedMethodRef(candidates.get(0).sourcePath(), candidates.get(0).indexedMethod().ref());
77-
AtomicInteger remaining = new AtomicInteger(Math.max(1, maxNodes));
7878
JsonObject structured = GraphSupport.graphMeta(direction, depth, maxNodes);
7979
structured.addProperty("resolved", true);
8080
structured.addProperty("indexBackend", "sqlite");
@@ -84,16 +84,18 @@ public ToolResult execute(JsonObject arguments) throws Exception {
8484
structured.add("root", methodJson(root));
8585

8686
StringBuilder text = new StringBuilder();
87-
text.append("Call chain for ").append(root.methodRef().displayName()).append(" @ ").append(root.sourcePath()).append('\n');
87+
text.append("Call chain for ").append(humanSignature(root.methodRef())).append(" @ ").append(root.sourcePath()).append('\n');
8888

8989
if ("callers".equals(direction) || "both".equals(direction)) {
9090
text.append("\nCallers:\n");
91-
JsonObject callersTree = buildTreeBfs(index, root, true, depth, remaining, new HashSet<>(), structured, text);
91+
int[] callerBudget = {maxNodes};
92+
JsonObject callersTree = buildTreeBfs(index, root, true, depth, callerBudget, new HashSet<>(), structured, text);
9293
structured.add("callers", callersTree);
9394
}
9495
if ("callees".equals(direction) || "both".equals(direction)) {
9596
text.append("\nCallees:\n");
96-
JsonObject calleesTree = buildTreeBfs(index, root, false, depth, remaining, new HashSet<>(), structured, text);
97+
int[] calleeBudget = {maxNodes};
98+
JsonObject calleesTree = buildTreeBfs(index, root, false, depth, calleeBudget, new HashSet<>(), structured, text);
9799
structured.add("callees", calleesTree);
98100
}
99101
structured.addProperty("queryMillis", (System.nanoTime() - startedAt) / 1_000_000L);
@@ -105,7 +107,7 @@ private static JsonObject buildTreeBfs(PersistentScopeIndex index,
105107
ScopedMethodRef root,
106108
boolean reverse,
107109
int depth,
108-
AtomicInteger remaining,
110+
int[] remaining,
109111
Set<ScopedMethodRef> visited,
110112
JsonObject graphMeta,
111113
StringBuilder text) throws Exception {
@@ -114,14 +116,14 @@ private static JsonObject buildTreeBfs(PersistentScopeIndex index,
114116
return rootNode;
115117
}
116118
rootNode.add("children", new JsonArray());
117-
text.append("- ").append(root.methodRef().displayName()).append(" @ ").append(root.sourcePath()).append('\n');
119+
text.append(humanSignature(root.methodRef())).append(" @ ").append(root.sourcePath()).append('\n');
118120

119-
record BfsFrame(ScopedMethodRef method, JsonObject parentNode, int currentDepth) {}
121+
record BfsFrame(ScopedMethodRef method, JsonObject parentNode, int currentDepth, String prefix) {}
120122
Deque<BfsFrame> queue = new ArrayDeque<>();
121-
queue.addLast(new BfsFrame(root, rootNode, 0));
123+
queue.addLast(new BfsFrame(root, rootNode, 0, ""));
122124
visited.add(root);
123125

124-
while (!queue.isEmpty() && remaining.get() > 0 && depth > 0) {
126+
while (!queue.isEmpty() && remaining[0] > 0) {
125127
BfsFrame frame = queue.removeFirst();
126128
if (frame.currentDepth() >= depth) {
127129
continue;
@@ -131,14 +133,19 @@ record BfsFrame(ScopedMethodRef method, JsonObject parentNode, int currentDepth)
131133
? index.incomingScoped(frame.method().methodRef())
132134
: index.outgoingScoped(frame.method().methodRef());
133135

134-
for (ScopedMethodRef neighbor : neighbors) {
135-
if (remaining.get() <= 0) {
136+
var eligible = new java.util.ArrayList<ScopedMethodRef>();
137+
for (ScopedMethodRef n : neighbors) {
138+
if (remaining[0] <= 0) break;
139+
if (!visited.contains(n)) eligible.add(n);
140+
}
141+
142+
for (int idx = 0; idx < eligible.size(); idx++) {
143+
if (remaining[0] <= 0) {
136144
graphMeta.addProperty("truncated", true);
137145
break;
138146
}
139-
if (visited.contains(neighbor)) {
140-
continue;
141-
}
147+
ScopedMethodRef neighbor = eligible.get(idx);
148+
boolean last = (idx == eligible.size() - 1);
142149
visited.add(neighbor);
143150

144151
JsonObject childNode = methodJson(neighbor);
@@ -148,17 +155,20 @@ record BfsFrame(ScopedMethodRef method, JsonObject parentNode, int currentDepth)
148155
}
149156
childNode.add("children", new JsonArray());
150157
frame.parentNode().getAsJsonArray("children").add(childNode);
151-
text.append(" ".repeat(frame.currentDepth() + 1))
152-
.append("- ").append(neighbor.methodRef().displayName())
158+
159+
String branch = last ? "└── " : "├── ";
160+
text.append(frame.prefix()).append(branch)
161+
.append(humanSignature(neighbor.methodRef()))
153162
.append(" @ ").append(neighbor.sourcePath()).append('\n');
154163

155164
if (frame.currentDepth() + 1 < depth) {
156-
queue.addLast(new BfsFrame(neighbor, childNode, frame.currentDepth() + 1));
165+
String childPrefix = frame.prefix() + (last ? " " : "│ ");
166+
queue.addLast(new BfsFrame(neighbor, childNode, frame.currentDepth() + 1, childPrefix));
157167
}
158168
}
159169
}
160170

161-
if (remaining.get() <= 0) {
171+
if (remaining[0] <= 0) {
162172
graphMeta.addProperty("truncated", true);
163173
}
164174
return rootNode;
@@ -172,10 +182,67 @@ private static JsonObject methodJson(ScopedMethodRef scopedMethod) {
172182
json.addProperty("displayOwner", method.owner().replace('/', '.'));
173183
json.addProperty("name", method.name());
174184
json.addProperty("descriptor", method.descriptor());
185+
var sig = formatSignature(method.descriptor());
186+
json.addProperty("params", sig.params());
187+
json.addProperty("returnType", sig.returnType());
188+
json.addProperty("signature", sig.params() + ": " + sig.returnType());
175189
json.addProperty("displayName", method.displayName());
176190
return json;
177191
}
178192

193+
private static String humanSignature(MethodRef method) {
194+
var sig = formatSignature(method.descriptor());
195+
return method.owner().replace('/', '.') + "." + method.name() + sig.params() + ": " + sig.returnType();
196+
}
197+
198+
private record MethodSignature(String params, String returnType) {}
199+
200+
private static MethodSignature formatSignature(String descriptor) {
201+
int end = descriptor.indexOf(')');
202+
if (end < 0) return new MethodSignature("()", "void");
203+
StringBuilder params = new StringBuilder("(");
204+
int i = 1;
205+
while (i < end) {
206+
if (params.length() > 1) params.append(", ");
207+
i = appendTypeName(descriptor, i, params);
208+
}
209+
params.append(')');
210+
StringBuilder ret = new StringBuilder();
211+
if (end + 1 < descriptor.length()) {
212+
appendTypeName(descriptor, end + 1, ret);
213+
} else {
214+
ret.append("void");
215+
}
216+
return new MethodSignature(params.toString(), ret.toString());
217+
}
218+
219+
private static int appendTypeName(String descriptor, int pos, StringBuilder sb) {
220+
char ch = descriptor.charAt(pos);
221+
return switch (ch) {
222+
case 'B' -> { sb.append("byte"); yield pos + 1; }
223+
case 'C' -> { sb.append("char"); yield pos + 1; }
224+
case 'D' -> { sb.append("double"); yield pos + 1; }
225+
case 'F' -> { sb.append("float"); yield pos + 1; }
226+
case 'I' -> { sb.append("int"); yield pos + 1; }
227+
case 'J' -> { sb.append("long"); yield pos + 1; }
228+
case 'S' -> { sb.append("short"); yield pos + 1; }
229+
case 'Z' -> { sb.append("boolean"); yield pos + 1; }
230+
case 'V' -> { sb.append("void"); yield pos + 1; }
231+
case 'L' -> {
232+
int end = descriptor.indexOf(';', pos);
233+
String full = descriptor.substring(pos + 1, end);
234+
sb.append(full.substring(full.lastIndexOf('/') + 1));
235+
yield end + 1;
236+
}
237+
case '[' -> {
238+
int nextPos = appendTypeName(descriptor, pos + 1, sb);
239+
sb.append("[]");
240+
yield nextPos;
241+
}
242+
default -> pos + 1;
243+
};
244+
}
245+
179246
private static String normalizeDirection(String direction) {
180247
String normalized = direction == null ? "both" : direction.trim().toLowerCase();
181248
return switch (normalized) {

0 commit comments

Comments
 (0)