devlensio 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/graph/edges/callEdges.js +97 -5
- package/dist/graph/edges/callEdges.test.js +154 -0
- package/dist/parser/extractors/classes.js +23 -4
- package/dist/parser/extractors/functions.d.ts +10 -3
- package/dist/parser/extractors/functions.js +135 -65
- package/dist/parser/extractors/hooks.js +5 -29
- package/dist/parser/extractors/objectMethods.js +3 -4
- package/dist/parser/index.js +11 -6
- package/dist/parser/overloads.d.ts +2 -0
- package/dist/parser/overloads.js +96 -0
- package/dist/parser/overloads.test.d.ts +1 -0
- package/dist/parser/overloads.test.js +175 -0
- package/dist/parser/typeUtils.d.ts +2 -0
- package/dist/parser/typeUtils.js +2 -0
- package/dist/types.d.ts +6 -0
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/calls.go +134 -2
- package/extractors/go/contract.go +1 -0
- package/extractors/go/exports.go +107 -0
- package/extractors/go/exports_test.go +64 -0
- package/extractors/go/extractor.go +1 -0
- package/extractors/go/lookup.go +63 -7
- package/extractors/go/nodes.go +28 -0
- package/extractors/go/parser.go +51 -8
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
- package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
- package/extractors/java/src/devlens/extractor/Parser.java +39 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
- package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
- package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
- package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
- package/extractors/python/devlens_extractors_python/extractor.py +7 -1
- package/extractors/python/devlens_extractors_python/lookup.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
- package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/src/calls.rs +27 -16
- package/extractors/rust/src/contract.rs +2 -0
- package/extractors/rust/src/exports.rs +387 -0
- package/extractors/rust/src/exports_test.rs +105 -0
- package/extractors/rust/src/extractor.rs +2 -0
- package/extractors/rust/src/lookup.rs +60 -0
- package/extractors/rust/src/main.rs +4 -0
- package/extractors/rust/src/nodes.rs +19 -0
- package/extractors/rust/src/parser.rs +66 -1
- package/extractors/rust/src/routes.rs +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
package devlens.extractor;
|
|
2
|
+
|
|
3
|
+
import devlens.extractor.Parser.CallInfo;
|
|
4
|
+
|
|
5
|
+
import java.nio.charset.StandardCharsets;
|
|
6
|
+
import java.security.MessageDigest;
|
|
7
|
+
import java.util.ArrayList;
|
|
8
|
+
import java.util.LinkedHashMap;
|
|
9
|
+
import java.util.LinkedHashSet;
|
|
10
|
+
import java.util.List;
|
|
11
|
+
import java.util.Map;
|
|
12
|
+
import java.util.Set;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Overload disambiguation — same-name methods in the same file (mirrors
|
|
16
|
+
* src/parser/overloads.ts from the TypeScript engine; keep in sync).
|
|
17
|
+
*
|
|
18
|
+
* Method node ids are `rel::Type.method`, so two overloads of `format`
|
|
19
|
+
* produced two nodes with ONE id and downstream dedupe silently dropped all
|
|
20
|
+
* but the first. Convention:
|
|
21
|
+
* - unique name in file → id unchanged
|
|
22
|
+
* - distinct arities → `#<arity>` (the declaration's own param count,
|
|
23
|
+
* never ordinals)
|
|
24
|
+
* - same arity, different param types → `#<arity>#<sig8>` where sig8 is the
|
|
25
|
+
* first 8 lowercase hex chars of sha-256 over the
|
|
26
|
+
* normalized signature "(T1,T2)"
|
|
27
|
+
* - signature-identical declarations collapse to one node (prefer the one
|
|
28
|
+
* with a body)
|
|
29
|
+
* Groups are per file; the same method name on different types (dotted names)
|
|
30
|
+
* never collides.
|
|
31
|
+
*
|
|
32
|
+
* Also hosts the overload-aware CALLS resolution ladder shared by the edge
|
|
33
|
+
* detectors: arity filter, then argTypes agreement, then first candidate.
|
|
34
|
+
*/
|
|
35
|
+
public final class Overloads {
|
|
36
|
+
|
|
37
|
+
private Overloads() {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalized param type from a metadata params entry ("Type name" — type
|
|
41
|
+
* first, name last; the name is the final space-separated token). Varargs
|
|
42
|
+
* normalize to "...Type" so signature keys and typeAgrees stay name-free.
|
|
43
|
+
*/
|
|
44
|
+
public static String paramTypeOf(String paramDecl) {
|
|
45
|
+
if (paramDecl == null) {
|
|
46
|
+
return "unknown";
|
|
47
|
+
}
|
|
48
|
+
String t = paramDecl.trim();
|
|
49
|
+
boolean varargs = t.contains("...");
|
|
50
|
+
t = t.replace("...", "");
|
|
51
|
+
int sp = t.lastIndexOf(' ');
|
|
52
|
+
if (sp >= 0) {
|
|
53
|
+
t = t.substring(0, sp).trim();
|
|
54
|
+
}
|
|
55
|
+
if (varargs) {
|
|
56
|
+
return "..." + t;
|
|
57
|
+
}
|
|
58
|
+
return t.isEmpty() ? "unknown" : t;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Normalized signature string "(T1,T2)" from a params list. */
|
|
62
|
+
public static String signatureKey(List<String> params) {
|
|
63
|
+
StringBuilder sb = new StringBuilder("(");
|
|
64
|
+
for (int i = 0; i < params.size(); i++) {
|
|
65
|
+
if (i > 0) sb.append(',');
|
|
66
|
+
sb.append(paramTypeOf(params.get(i)));
|
|
67
|
+
}
|
|
68
|
+
return sb.append(')').toString();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** First 8 lowercase hex chars of sha-256 over the signature string. */
|
|
72
|
+
public static String sig8(String signatureKey) {
|
|
73
|
+
try {
|
|
74
|
+
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
75
|
+
byte[] digest = md.digest(signatureKey.getBytes(StandardCharsets.UTF_8));
|
|
76
|
+
StringBuilder sb = new StringBuilder(8);
|
|
77
|
+
for (int i = 0; i < 4; i++) {
|
|
78
|
+
sb.append(String.format("%02x", digest[i]));
|
|
79
|
+
}
|
|
80
|
+
return sb.toString();
|
|
81
|
+
} catch (Exception e) {
|
|
82
|
+
return "00000000";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@SuppressWarnings("unchecked")
|
|
87
|
+
private static List<String> paramsOf(Map<String, Object> methodNode) {
|
|
88
|
+
Map<String, Object> metadata = (Map<String, Object>) methodNode.get("metadata");
|
|
89
|
+
Object params = metadata == null ? null : metadata.get("params");
|
|
90
|
+
return params instanceof List ? (List<String>) params : new ArrayList<>();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private static boolean hasBody(Map<String, Object> methodNode) {
|
|
94
|
+
Map<String, Object> metadata = (Map<String, Object>) methodNode.get("metadata");
|
|
95
|
+
return metadata == null || !Boolean.TRUE.equals(metadata.get("isAbstract"))
|
|
96
|
+
|| Boolean.TRUE.equals(metadata.get("isConstructor"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static int arityOf(Map<String, Object> methodNode) {
|
|
100
|
+
return paramsOf(methodNode).size();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Rewrites METHOD node ids for same-name groups within one file, marks
|
|
105
|
+
* collapsed duplicates, and remaps methodCallFacts keys to the new ids.
|
|
106
|
+
* Deterministic: groups in insertion (line) order, no ordinal suffixes.
|
|
107
|
+
*/
|
|
108
|
+
public static void disambiguate(List<Map<String, Object>> nodes,
|
|
109
|
+
Map<String, List<Parser.CallInfo>> methodCallFacts) {
|
|
110
|
+
Map<String, List<Map<String, Object>>> groups = new LinkedHashMap<>();
|
|
111
|
+
for (Map<String, Object> node : nodes) {
|
|
112
|
+
if (!"METHOD".equals(node.get("type"))) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
String key = node.get("filePath") + "|" + node.get("name");
|
|
116
|
+
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(node);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (List<Map<String, Object>> group : groups.values()) {
|
|
120
|
+
if (group.size() < 2) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Collapse signature-identical declarations, prefer the one with a body.
|
|
125
|
+
Map<String, Map<String, Object>> bySignature = new LinkedHashMap<>();
|
|
126
|
+
Map<Map<String, Object>, String> keys = new LinkedHashMap<>();
|
|
127
|
+
for (Map<String, Object> node : group) {
|
|
128
|
+
String key = signatureKey(paramsOf(node));
|
|
129
|
+
keys.put(node, key);
|
|
130
|
+
Map<String, Object> existing = bySignature.get(key);
|
|
131
|
+
if (existing == null) {
|
|
132
|
+
bySignature.put(key, node);
|
|
133
|
+
} else if (!hasBody(existing) && hasBody(node)) {
|
|
134
|
+
bySignature.put(key, node);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
List<Map<String, Object>> survivors = new ArrayList<>(bySignature.values());
|
|
138
|
+
if (survivors.size() < group.size()) {
|
|
139
|
+
Set<Map<String, Object>> kept = new LinkedHashSet<>(survivors);
|
|
140
|
+
List<Map<String, Object>> dropped = new ArrayList<>();
|
|
141
|
+
for (Map<String, Object> node : group) {
|
|
142
|
+
if (!kept.contains(node)) {
|
|
143
|
+
((Map<String, Object>) node.get("metadata")).put("isDuplicateOverload", true);
|
|
144
|
+
dropped.add(node);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
nodes.removeAll(dropped);
|
|
148
|
+
for (Map<String, Object> node : dropped) {
|
|
149
|
+
methodCallFacts.remove(node.get("id"));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (survivors.size() < 2) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
Set<Integer> arities = new LinkedHashSet<>();
|
|
157
|
+
for (Map<String, Object> node : survivors) {
|
|
158
|
+
arities.add(arityOf(node));
|
|
159
|
+
}
|
|
160
|
+
boolean useArityOnly = arities.size() == survivors.size();
|
|
161
|
+
String relPath = (String) survivors.get(0).get("filePath");
|
|
162
|
+
String name = (String) survivors.get(0).get("name");
|
|
163
|
+
|
|
164
|
+
for (Map<String, Object> node : survivors) {
|
|
165
|
+
int arity = arityOf(node);
|
|
166
|
+
String suffix = "#" + arity;
|
|
167
|
+
if (!useArityOnly) {
|
|
168
|
+
suffix = "#" + arity + "#" + sig8(keys.get(node));
|
|
169
|
+
}
|
|
170
|
+
String newId = relPath + "::" + name + suffix;
|
|
171
|
+
if (!newId.equals(node.get("id"))) {
|
|
172
|
+
List<Parser.CallInfo> facts = methodCallFacts.remove(node.get("id"));
|
|
173
|
+
node.put("id", newId);
|
|
174
|
+
if (facts != null) {
|
|
175
|
+
methodCallFacts.put(newId, facts);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── overload-aware CALLS resolution ladder ───────────────────────
|
|
183
|
+
|
|
184
|
+
/** Arity compatibility: varargs accept any count >= required. */
|
|
185
|
+
public static boolean arityMatches(Map<String, Object> candidate, int argCount) {
|
|
186
|
+
List<String> params = paramsOf(candidate);
|
|
187
|
+
int total = params.size();
|
|
188
|
+
boolean hasVarargs = false;
|
|
189
|
+
for (String p : params) {
|
|
190
|
+
if (paramTypeOf(p).startsWith("...")) {
|
|
191
|
+
hasVarargs = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
int required = hasVarargs ? total - 1 : total;
|
|
195
|
+
return argCount >= required && (hasVarargs || argCount <= total);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Loose arg-type agreement: exact or containment, unknown never matches. */
|
|
199
|
+
public static boolean typeAgrees(String argType, String paramType) {
|
|
200
|
+
if (argType == null || argType.isEmpty() || "unknown".equals(argType) || paramType == null) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return paramType.equals(argType) || paramType.contains(argType);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public static int typeScore(Map<String, Object> candidate, List<String> argTypes) {
|
|
207
|
+
List<String> params = paramsOf(candidate);
|
|
208
|
+
int score = 0;
|
|
209
|
+
for (int i = 0; i < argTypes.size(); i++) {
|
|
210
|
+
if (i < params.size() && typeAgrees(argTypes.get(i), paramTypeOf(params.get(i)))) {
|
|
211
|
+
score++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return score;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Picks the right overload among same-name candidates of one class using
|
|
219
|
+
* the call site's argument shape. Ladder (mirrors the TypeScript
|
|
220
|
+
* resolveOverloadTarget):
|
|
221
|
+
* 1. single candidate → "name"
|
|
222
|
+
* 2. arity filter
|
|
223
|
+
* 3. argTypes tiebreak (unique best score) → "signature"
|
|
224
|
+
* 4. still ambiguous → first candidate → "name"
|
|
225
|
+
* Empty candidates → null.
|
|
226
|
+
*/
|
|
227
|
+
public static Pick resolveOverload(Map<String, Map<String, Object>> nodeById,
|
|
228
|
+
List<String> candidateIds, Parser.CallInfo ci) {
|
|
229
|
+
if (candidateIds == null || candidateIds.isEmpty()) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (candidateIds.size() == 1) {
|
|
233
|
+
return new Pick(candidateIds.get(0), "name");
|
|
234
|
+
}
|
|
235
|
+
int argCount = ci == null ? -1 : ci.argCount;
|
|
236
|
+
List<String> pool = new ArrayList<>();
|
|
237
|
+
if (ci != null && !ci.hasSpread && argCount >= 0) {
|
|
238
|
+
for (String id : candidateIds) {
|
|
239
|
+
Map<String, Object> node = nodeById.get(id);
|
|
240
|
+
if (node == null || arityMatches(node, argCount)) {
|
|
241
|
+
pool.add(id);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (pool.isEmpty()) {
|
|
245
|
+
pool.addAll(candidateIds);
|
|
246
|
+
}
|
|
247
|
+
if (pool.size() == 1) {
|
|
248
|
+
return new Pick(pool.get(0), "arity");
|
|
249
|
+
}
|
|
250
|
+
List<String> argTypes = ci.argTypes == null ? new ArrayList<>() : ci.argTypes;
|
|
251
|
+
boolean anyTyped = false;
|
|
252
|
+
for (String t : argTypes) {
|
|
253
|
+
if (t != null && !t.isEmpty() && !"unknown".equals(t)) {
|
|
254
|
+
anyTyped = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (anyTyped) {
|
|
259
|
+
String best = pool.get(0);
|
|
260
|
+
int bestScore = typeScore(nodeById.get(best), argTypes);
|
|
261
|
+
int ties = 1;
|
|
262
|
+
for (int i = 1; i < pool.size(); i++) {
|
|
263
|
+
int s = typeScore(nodeById.get(pool.get(i)), argTypes);
|
|
264
|
+
if (s > bestScore) {
|
|
265
|
+
best = pool.get(i);
|
|
266
|
+
bestScore = s;
|
|
267
|
+
ties = 1;
|
|
268
|
+
} else if (s == bestScore) {
|
|
269
|
+
ties++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (bestScore > 0 && ties == 1) {
|
|
273
|
+
return new Pick(best, "signature");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return new Pick(pool.isEmpty() ? candidateIds.get(0) : pool.get(0), "name");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Resolved overload target + the confidence tier that picked it. */
|
|
281
|
+
public static final class Pick {
|
|
282
|
+
public final String id;
|
|
283
|
+
public final String matchedBy;
|
|
284
|
+
|
|
285
|
+
public Pick(String id, String matchedBy) {
|
|
286
|
+
this.id = id;
|
|
287
|
+
this.matchedBy = matchedBy;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -76,6 +76,9 @@ public final class Parser {
|
|
|
76
76
|
public String receiverName; // root identifier of the scope (or null)
|
|
77
77
|
public int line;
|
|
78
78
|
public String resolvedTarget; // FQCN.method when the symbol solver hit, else null
|
|
79
|
+
public int argCount; // argument count at the call site
|
|
80
|
+
public List<String> argTypes = new ArrayList<>(); // cheap arg type tags ("String", "int", "unknown")
|
|
81
|
+
public boolean hasSpread = false; // Java has no call-site spread; kept for contract parity
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
public static final class MethodInfo {
|
|
@@ -83,6 +86,7 @@ public final class Parser {
|
|
|
83
86
|
public boolean isConstructor;
|
|
84
87
|
public boolean isStatic;
|
|
85
88
|
public boolean isAbstract;
|
|
89
|
+
public boolean isPublic;
|
|
86
90
|
public List<String> annotations = new ArrayList<>();
|
|
87
91
|
public Map<String, String> annotationValues = new LinkedHashMap<>();
|
|
88
92
|
public String returnType = "";
|
|
@@ -112,6 +116,7 @@ public final class Parser {
|
|
|
112
116
|
public boolean isEnum;
|
|
113
117
|
public boolean isRecord;
|
|
114
118
|
public boolean isAbstract;
|
|
119
|
+
public boolean isPublic;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
public static final class ParsedFile {
|
|
@@ -168,6 +173,7 @@ public final class Parser {
|
|
|
168
173
|
private static TypeInfo extractType(TypeDeclaration<?> td, ParsedFile pf, String outerDotted) {
|
|
169
174
|
TypeInfo t = new TypeInfo();
|
|
170
175
|
String simple = td.getNameAsString();
|
|
176
|
+
t.isPublic = td.isPublic();
|
|
171
177
|
t.dottedName = outerDotted == null ? simple : outerDotted + "." + simple;
|
|
172
178
|
t.startLine = lineOf(td, true);
|
|
173
179
|
t.endLine = lineOf(td, false);
|
|
@@ -268,6 +274,7 @@ public final class Parser {
|
|
|
268
274
|
private static MethodInfo extractMethod(MethodDeclaration md, ParsedFile pf, boolean inInterface) {
|
|
269
275
|
MethodInfo m = new MethodInfo();
|
|
270
276
|
m.name = md.getNameAsString();
|
|
277
|
+
m.isPublic = inInterface || md.isPublic();
|
|
271
278
|
m.isStatic = md.isStatic();
|
|
272
279
|
m.isAbstract = inInterface || md.isAbstract();
|
|
273
280
|
m.returnType = simpleTypeName(md.getTypeAsString());
|
|
@@ -307,6 +314,10 @@ public final class Parser {
|
|
|
307
314
|
ci.receiverName = null;
|
|
308
315
|
ci.name = call.getNameAsString();
|
|
309
316
|
}
|
|
317
|
+
for (Expression arg : call.getArguments()) {
|
|
318
|
+
ci.argTypes.add(argTypeName(arg));
|
|
319
|
+
}
|
|
320
|
+
ci.argCount = ci.argTypes.size();
|
|
310
321
|
// Best-effort symbol-solver target: pkg.Class.method (or null).
|
|
311
322
|
try {
|
|
312
323
|
ResolvedMethodDeclaration r = call.resolve();
|
|
@@ -319,6 +330,34 @@ public final class Parser {
|
|
|
319
330
|
return calls;
|
|
320
331
|
}
|
|
321
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Cheap argument type tag in the same vocabulary as declared param types:
|
|
335
|
+
* literals map directly, anything else goes through the symbol solver
|
|
336
|
+
* (best-effort), falling back to "unknown".
|
|
337
|
+
*/
|
|
338
|
+
private static String argTypeName(Expression arg) {
|
|
339
|
+
if (arg instanceof StringLiteralExpr) return "String";
|
|
340
|
+
if (arg instanceof com.github.javaparser.ast.expr.IntegerLiteralExpr) return "int";
|
|
341
|
+
if (arg instanceof com.github.javaparser.ast.expr.LongLiteralExpr) return "long";
|
|
342
|
+
if (arg instanceof com.github.javaparser.ast.expr.DoubleLiteralExpr) return "double";
|
|
343
|
+
if (arg instanceof com.github.javaparser.ast.expr.BooleanLiteralExpr) return "boolean";
|
|
344
|
+
if (arg instanceof com.github.javaparser.ast.expr.CharLiteralExpr) return "char";
|
|
345
|
+
if (arg instanceof com.github.javaparser.ast.expr.NullLiteralExpr) return "null";
|
|
346
|
+
if (arg instanceof ObjectCreationExpr nce) {
|
|
347
|
+
return simpleTypeName(nce.getTypeAsString());
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
if (TypeSolverFactory.solver() == null) {
|
|
351
|
+
return "unknown";
|
|
352
|
+
}
|
|
353
|
+
com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade facade =
|
|
354
|
+
com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade.get(TypeSolverFactory.solver());
|
|
355
|
+
return simpleTypeName(facade.getType(arg).describe());
|
|
356
|
+
} catch (Throwable ignored) {
|
|
357
|
+
return "unknown";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
322
361
|
// ─────────────────────────── helpers ───────────────────────────
|
|
323
362
|
|
|
324
363
|
private static int lineOf(Node n, boolean start) {
|
|
@@ -24,12 +24,21 @@ public final class TypeSolverFactory {
|
|
|
24
24
|
|
|
25
25
|
private TypeSolverFactory() {}
|
|
26
26
|
|
|
27
|
+
private static CombinedTypeSolver combinedSolver;
|
|
28
|
+
|
|
29
|
+
/** Shared solver instance (configured by {@link #configure}) for
|
|
30
|
+
* best-effort expression type resolution. Null before configure. */
|
|
31
|
+
public static CombinedTypeSolver solver() {
|
|
32
|
+
return combinedSolver;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
public static void configure(Path repoRoot) {
|
|
28
36
|
CombinedTypeSolver combined = new CombinedTypeSolver();
|
|
29
37
|
combined.add(new ReflectionTypeSolver());
|
|
30
38
|
for (Path root : SourceWalker.sourceRoots(repoRoot)) {
|
|
31
39
|
combined.add(new JavaParserTypeSolver(root));
|
|
32
40
|
}
|
|
41
|
+
combinedSolver = combined;
|
|
33
42
|
// JavaParser 3.26+ removed StaticJavaParser.setSymbolSolver — the
|
|
34
43
|
// resolver is configured via ParserConfiguration instead.
|
|
35
44
|
// JAVA_21 level is REQUIRED: the default level (<14) makes records
|
|
@@ -2,6 +2,7 @@ package devlens.extractor.edges;
|
|
|
2
2
|
|
|
3
3
|
import devlens.extractor.Contract;
|
|
4
4
|
import devlens.extractor.LookupMaps;
|
|
5
|
+
import devlens.extractor.Overloads;
|
|
5
6
|
import devlens.extractor.Parser;
|
|
6
7
|
import devlens.extractor.ThirdParty;
|
|
7
8
|
|
|
@@ -45,17 +46,19 @@ public final class Calls {
|
|
|
45
46
|
List<Map<String, String>> resolvedDetails = new ArrayList<>();
|
|
46
47
|
|
|
47
48
|
for (Parser.CallInfo ci : entry.getValue()) {
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
Map<String, Object>
|
|
51
|
-
|
|
49
|
+
Overloads.Pick pick = resolveOne(ci, lookup, relPath, parentClass, pkg, methodNodeId);
|
|
50
|
+
if (pick != null) {
|
|
51
|
+
Map<String, Object> edgeMeta = new LinkedHashMap<>();
|
|
52
|
+
edgeMeta.put("line", ci.line);
|
|
53
|
+
edgeMeta.put("matchedBy", pick.matchedBy);
|
|
54
|
+
Map<String, Object> edge = Contract.edge("CALLS", methodNodeId, pick.id, edgeMeta);
|
|
52
55
|
edgesOut.add(edge);
|
|
53
|
-
resolvedCalls.add(
|
|
56
|
+
resolvedCalls.add(pick.id);
|
|
54
57
|
// details for OrmEdges (repo R/W classification)
|
|
55
58
|
Map<String, String> detail = new LinkedHashMap<>();
|
|
56
|
-
detail.put("target",
|
|
59
|
+
detail.put("target", pick.id);
|
|
57
60
|
detail.put("method", ci.methodName);
|
|
58
|
-
detail.put("class", classNodeOf(
|
|
61
|
+
detail.put("class", classNodeOf(pick.id));
|
|
59
62
|
resolvedDetails.add(detail);
|
|
60
63
|
}
|
|
61
64
|
}
|
|
@@ -67,9 +70,9 @@ public final class Calls {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
private static
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
private static Overloads.Pick resolveOne(Parser.CallInfo ci, LookupMaps lookup,
|
|
74
|
+
String relPath, String parentClass,
|
|
75
|
+
String pkg, String methodNodeId) {
|
|
73
76
|
// 1. symbol-solver target
|
|
74
77
|
if (ci.resolvedTarget != null) {
|
|
75
78
|
int lastDot = ci.resolvedTarget.lastIndexOf('.');
|
|
@@ -81,13 +84,9 @@ public final class Calls {
|
|
|
81
84
|
String rel = lookup.typeMap.get(classFqcn);
|
|
82
85
|
if (rel != null) {
|
|
83
86
|
String dotted = lookup.typeDottedMap.get(classFqcn);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return methodId;
|
|
88
|
-
}
|
|
89
|
-
// constructor call resolved via class — no method node for <init> chains
|
|
90
|
-
return null;
|
|
87
|
+
// no node for the resolved method (e.g. <init> chains) → null,
|
|
88
|
+
// never a name-based fallback from a solver-confirmed class
|
|
89
|
+
return pickOverload(lookup, rel + "::" + dotted, method, ci);
|
|
91
90
|
}
|
|
92
91
|
// external (no jars on classpath in V1) → fall through to name-based
|
|
93
92
|
}
|
|
@@ -95,19 +94,15 @@ public final class Calls {
|
|
|
95
94
|
String receiver = ci.receiverName;
|
|
96
95
|
// 2a. this./plain call → same-class method
|
|
97
96
|
if (receiver == null || "this".equals(receiver) || "super".equals(receiver)) {
|
|
98
|
-
return sameClassMethod(methodNodeId, parentClass, ci
|
|
97
|
+
return sameClassMethod(methodNodeId, parentClass, ci, lookup);
|
|
99
98
|
}
|
|
100
99
|
// 2d. alias in symbolMap (imported type or third-party member)
|
|
101
100
|
String aliasTarget = lookup.symbolMap(relPath).get(receiver);
|
|
102
101
|
if (aliasTarget != null) {
|
|
103
102
|
if (aliasTarget.startsWith("[mvn]")) {
|
|
104
|
-
return aliasTarget; // chain rule: named-import root → alias itself
|
|
105
|
-
}
|
|
106
|
-
String methodId = aliasTarget + "." + ci.methodName;
|
|
107
|
-
if (lookup.nodeById.containsKey(methodId)) {
|
|
108
|
-
return methodId;
|
|
103
|
+
return new Overloads.Pick(aliasTarget, "name"); // chain rule: named-import root → alias itself
|
|
109
104
|
}
|
|
110
|
-
return
|
|
105
|
+
return pickOverload(lookup, aliasTarget, ci.methodName, ci);
|
|
111
106
|
}
|
|
112
107
|
// 2b/2c. receiver is a field or param of the owning method's class
|
|
113
108
|
String receiverType = fieldOrParamType(methodNodeId, lookup, receiver);
|
|
@@ -115,11 +110,11 @@ public final class Calls {
|
|
|
115
110
|
String classId = resolveTypeRef(receiverType, lookup, relPath, pkg);
|
|
116
111
|
if (classId != null) {
|
|
117
112
|
if (classId.startsWith("[mvn]")) {
|
|
118
|
-
return classId;
|
|
113
|
+
return new Overloads.Pick(classId, "name");
|
|
119
114
|
}
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
return
|
|
115
|
+
Overloads.Pick pick = pickOverload(lookup, classId, ci.methodName, ci);
|
|
116
|
+
if (pick != null) {
|
|
117
|
+
return pick;
|
|
123
118
|
}
|
|
124
119
|
// Spring Data repos inherit findAll/save/findById/... — not
|
|
125
120
|
// declared in the source file. Edge to the repo interface
|
|
@@ -128,7 +123,7 @@ public final class Calls {
|
|
|
128
123
|
if (classNode != null) {
|
|
129
124
|
Map<String, Object> cm = (Map<String, Object>) classNode.get("metadata");
|
|
130
125
|
if (Boolean.TRUE.equals(cm.get("isRepository"))) {
|
|
131
|
-
return classId;
|
|
126
|
+
return new Overloads.Pick(classId, "name");
|
|
132
127
|
}
|
|
133
128
|
}
|
|
134
129
|
return null;
|
|
@@ -138,16 +133,27 @@ public final class Calls {
|
|
|
138
133
|
return null;
|
|
139
134
|
}
|
|
140
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Overload-aware method lookup on a class: the methodOverloads index
|
|
138
|
+
* holds ALL same-name METHOD node ids for `classId.methodName` (unsuffixed
|
|
139
|
+
* key); resolution picks the matching overload via the call's argument
|
|
140
|
+
* shape. Null when the class declares no such method.
|
|
141
|
+
*/
|
|
142
|
+
private static Overloads.Pick pickOverload(LookupMaps lookup, String classId,
|
|
143
|
+
String methodName, Parser.CallInfo ci) {
|
|
144
|
+
List<String> candidates = lookup.methodOverloads.get(classId + "." + methodName);
|
|
145
|
+
if (candidates == null || candidates.isEmpty()) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return Overloads.resolveOverload(lookup.nodeById, candidates, ci);
|
|
149
|
+
}
|
|
150
|
+
|
|
141
151
|
/** Method on the SAME class (or inherited — same file). */
|
|
142
|
-
private static
|
|
143
|
-
|
|
152
|
+
private static Overloads.Pick sameClassMethod(String methodNodeId, String parentClass,
|
|
153
|
+
Parser.CallInfo ci, LookupMaps lookup) {
|
|
144
154
|
int colon = methodNodeId.lastIndexOf("::");
|
|
145
155
|
String prefix = colon < 0 ? methodNodeId : methodNodeId.substring(0, colon);
|
|
146
|
-
|
|
147
|
-
if (lookup.nodeById.containsKey(id)) {
|
|
148
|
-
return id;
|
|
149
|
-
}
|
|
150
|
-
return null;
|
|
156
|
+
return pickOverload(lookup, prefix + "::" + parentClass, ci.methodName, ci);
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
/**
|
|
@@ -63,7 +63,7 @@ public final class Routes {
|
|
|
63
63
|
String methodPath = pathValue(m.annotationValues.get(ann));
|
|
64
64
|
String verb = verbFor(ann, m.annotationValues.get(ann + ".method"));
|
|
65
65
|
String urlPath = join(base, methodPath);
|
|
66
|
-
emit(pf, t, m, verb, urlPath, fw, result);
|
|
66
|
+
emit(lookup, pf, t, m, verb, urlPath, fw, result);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -112,8 +112,9 @@ public final class Routes {
|
|
|
112
112
|
return out;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
private static void emit(Parser.ParsedFile pf, Parser.TypeInfo t,
|
|
116
|
-
String verb, String urlPath, String fw,
|
|
115
|
+
private static void emit(LookupMaps lookup, Parser.ParsedFile pf, Parser.TypeInfo t,
|
|
116
|
+
Parser.MethodInfo m, String verb, String urlPath, String fw,
|
|
117
|
+
RouteResult result) {
|
|
117
118
|
String key = pf.relPath + "|" + verb + "|" + urlPath;
|
|
118
119
|
if (!result.seen.add(key)) {
|
|
119
120
|
return;
|
|
@@ -126,7 +127,13 @@ public final class Routes {
|
|
|
126
127
|
params.add(matcher.group(1));
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
// Overload-aware: the index holds the actual (possibly suffix-qualified)
|
|
131
|
+
// METHOD node id; first candidate wins for overloaded handlers.
|
|
129
132
|
String handlerId = pf.relPath + "::" + t.dottedName + "." + m.name;
|
|
133
|
+
List<String> overloadIds = lookup.methodOverloads.get(handlerId);
|
|
134
|
+
if (overloadIds != null && !overloadIds.isEmpty()) {
|
|
135
|
+
handlerId = overloadIds.get(0);
|
|
136
|
+
}
|
|
130
137
|
Map<String, Object> routeNode = Contract.routeNode(pf.relPath, verb, urlPath,
|
|
131
138
|
dynamic, params, fw, m.name);
|
|
132
139
|
Map<String, Object> backendRoute = Contract.backendRoute(urlPath, pf.relPath,
|