devlensio 0.6.2 → 1.0.1
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/README.md +89 -31
- package/dist/extractors/detectLanguage.d.ts +2 -0
- package/dist/extractors/detectLanguage.js +33 -0
- package/dist/extractors/index.d.ts +6 -0
- package/dist/extractors/index.js +155 -0
- package/dist/extractors/runner.d.ts +7 -0
- package/dist/extractors/runner.js +194 -0
- package/dist/extractors/types.d.ts +37 -0
- package/dist/extractors/types.js +8 -0
- package/dist/graph/buildLookup.d.ts +1 -0
- package/dist/graph/buildLookup.js +2 -1
- package/dist/graph/edges/callEdges.js +19 -5
- package/dist/graph/edges/callEdges.test.d.ts +1 -0
- package/dist/graph/edges/callEdges.test.js +200 -0
- package/dist/graph/edges/importEdges.js +12 -0
- package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
- package/dist/graph/edges/inheritanceEdges.js +73 -0
- package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
- package/dist/graph/edges/inheritanceEdges.test.js +140 -0
- package/dist/graph/index.js +4 -0
- package/dist/parser/classes.test.d.ts +1 -0
- package/dist/parser/classes.test.js +360 -0
- package/dist/parser/extractors/classes.d.ts +5 -0
- package/dist/parser/extractors/classes.js +241 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +7 -1
- package/dist/pipeline/index.d.ts +2 -1
- package/dist/pipeline/index.js +30 -42
- package/dist/scoring/index.js +8 -0
- package/dist/scoring/index.test.js +43 -0
- package/dist/scoring/nodeScorer.js +2 -0
- package/dist/scoring/pruneDisconnected.d.ts +8 -0
- package/dist/scoring/pruneDisconnected.js +66 -0
- package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
- package/dist/scoring/pruneDisconnected.test.js +123 -0
- package/dist/summarizer/prompts.d.ts +1 -1
- package/dist/types.d.ts +5 -5
- 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/build.mjs +43 -0
- package/extractors/go/calls.go +289 -0
- package/extractors/go/contract.go +140 -0
- package/extractors/go/extractor.go +161 -0
- package/extractors/go/fingerprint.go +167 -0
- package/extractors/go/go.mod +3 -0
- package/extractors/go/imports.go +90 -0
- package/extractors/go/inheritance.go +138 -0
- package/extractors/go/lookup.go +106 -0
- package/extractors/go/main.go +57 -0
- package/extractors/go/nodes.go +177 -0
- package/extractors/go/orm_edges.go +277 -0
- package/extractors/go/parser.go +469 -0
- package/extractors/go/routes.go +521 -0
- package/extractors/go/tests.go +45 -0
- package/extractors/go/thirdparty.go +138 -0
- package/extractors/go/typeload.go +178 -0
- package/extractors/go/walker.go +67 -0
- package/extractors/java/build.mjs +90 -0
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/Contract.java +171 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
- package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
- package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
- package/extractors/java/src/devlens/extractor/Main.java +66 -0
- package/extractors/java/src/devlens/extractor/Parser.java +522 -0
- package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
- package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
- package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
- package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
- package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
- package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
- package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
- package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
- package/extractors/python/devlens_extractors_python/__init__.py +3 -0
- package/extractors/python/devlens_extractors_python/__main__.py +33 -0
- package/extractors/python/devlens_extractors_python/contract.py +94 -0
- package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
- package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
- package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
- package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
- package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
- package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
- package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
- package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
- package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
- package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
- package/extractors/python/devlens_extractors_python/extractor.py +107 -0
- package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
- package/extractors/python/devlens_extractors_python/lookup.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
- package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
- package/extractors/python/devlens_extractors_python/third_party.py +103 -0
- package/extractors/python/pyproject.toml +16 -0
- package/extractors/python/setup.mjs +54 -0
- package/extractors/rust/Cargo.toml +29 -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/build.mjs +85 -0
- package/extractors/rust/src/calls.rs +295 -0
- package/extractors/rust/src/contract.rs +236 -0
- package/extractors/rust/src/enrich.rs +107 -0
- package/extractors/rust/src/extractor.rs +199 -0
- package/extractors/rust/src/fingerprint.rs +238 -0
- package/extractors/rust/src/imports.rs +101 -0
- package/extractors/rust/src/inheritance.rs +233 -0
- package/extractors/rust/src/lookup.rs +226 -0
- package/extractors/rust/src/main.rs +53 -0
- package/extractors/rust/src/module_map.rs +131 -0
- package/extractors/rust/src/nodes.rs +174 -0
- package/extractors/rust/src/orm_edges.rs +125 -0
- package/extractors/rust/src/parser.rs +859 -0
- package/extractors/rust/src/routes.rs +1265 -0
- package/extractors/rust/src/tests.rs +109 -0
- package/extractors/rust/src/thirdparty.rs +114 -0
- package/extractors/rust/src/walker.rs +82 -0
- package/package.json +22 -4
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package devlens.extractor;
|
|
2
|
+
|
|
3
|
+
import java.util.ArrayList;
|
|
4
|
+
import java.util.LinkedHashMap;
|
|
5
|
+
import java.util.LinkedHashSet;
|
|
6
|
+
import java.util.List;
|
|
7
|
+
import java.util.Map;
|
|
8
|
+
import java.util.Set;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ThirdParty — [mvn]/<package-prefix> node registry (mirrors third_party.py).
|
|
12
|
+
*
|
|
13
|
+
* Node key form: first TWO segments of the import package — e.g. import
|
|
14
|
+
* org.springframework.web.bind.annotation.* → [mvn]/org.springframework.
|
|
15
|
+
* Member nodes: [mvn]/org.springframework::RestTemplate for named imports.
|
|
16
|
+
* This mirrors Python's [pip]/<top-level-name> convention: the node key is
|
|
17
|
+
* the IMPORT name, not the Maven artifact (g:a). The frontend selects from
|
|
18
|
+
* fingerprint.rawDependencies (g:a) — the gating normalizes both sides:
|
|
19
|
+
* "org.springframework.boot:spring-boot-starter-web" → prefix
|
|
20
|
+
* "org.springframework" (first two segments of the groupId).
|
|
21
|
+
*
|
|
22
|
+
* JDK packages are skipped entirely (stdlib tier — no node, no edge).
|
|
23
|
+
* `allowed` gates everything: empty set → ZERO third-party nodes.
|
|
24
|
+
*/
|
|
25
|
+
public final class ThirdParty {
|
|
26
|
+
|
|
27
|
+
private static final Set<String> JDK_PREFIXES = Set.of(
|
|
28
|
+
"java.", "javax.", "jdk.", "sun.", "com.sun.", "org.w3c.", "org.xml.");
|
|
29
|
+
|
|
30
|
+
private static final Set<String> RUNTIME_PREFIXES = Set.of(
|
|
31
|
+
"org.springframework", "com.fasterxml.jackson", "org.hibernate",
|
|
32
|
+
"jakarta.persistence", "jakarta.validation", "jakarta.servlet",
|
|
33
|
+
"org.slf4j", "io.swagger", "org.apache.commons", "org.apache.http",
|
|
34
|
+
"org.apache.kafka", "com.google.common", "com.google.gson",
|
|
35
|
+
"org.projectlombok", "org.mapstruct", "com.querydsl", "io.micrometer",
|
|
36
|
+
"org.flywaydb", "org.liquibase", "org.mybatis", "org.elasticsearch",
|
|
37
|
+
"com.rabbitmq", "org.apache.activemq", "com.zaxxer", "redis.clients");
|
|
38
|
+
|
|
39
|
+
private static final Set<String> DEVTOOL_PREFIXES = Set.of(
|
|
40
|
+
"org.junit", "org.mockito", "org.assertj", "org.testng",
|
|
41
|
+
"org.testcontainers", "io.rest-assured", "org.seleniumhq");
|
|
42
|
+
|
|
43
|
+
private final Map<String, Map<String, Object>> nodes = new LinkedHashMap<>();
|
|
44
|
+
private final Map<String, String> rawDependencies;
|
|
45
|
+
private final Set<String> allowed = new LinkedHashSet<>();
|
|
46
|
+
|
|
47
|
+
public ThirdParty(Map<String, String> rawDependencies, List<String> allowedSelection) {
|
|
48
|
+
this.rawDependencies = rawDependencies == null ? Map.of() : rawDependencies;
|
|
49
|
+
for (String sel : allowedSelection) {
|
|
50
|
+
if (sel == null || sel.isBlank()) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (sel.contains(":")) {
|
|
54
|
+
// g:a form from rawDependencies → first two segments of groupId
|
|
55
|
+
String group = sel.substring(0, sel.indexOf(':'));
|
|
56
|
+
allowed.add(packagePrefix(group));
|
|
57
|
+
} else {
|
|
58
|
+
allowed.add(sel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** First two segments: org.springframework.boot → org.springframework */
|
|
64
|
+
public static String packagePrefix(String pkg) {
|
|
65
|
+
String[] parts = pkg.split("\\.");
|
|
66
|
+
if (parts.length <= 2) {
|
|
67
|
+
return pkg;
|
|
68
|
+
}
|
|
69
|
+
return parts[0] + "." + parts[1];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public static boolean isJdk(String pkg) {
|
|
73
|
+
for (String p : JDK_PREFIXES) {
|
|
74
|
+
if (pkg.startsWith(p)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public boolean permitted(String prefix) {
|
|
82
|
+
return allowed.contains(prefix);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private Map<String, Object> baseMetadata(String prefix) {
|
|
86
|
+
String version = rawDependencies.entrySet().stream()
|
|
87
|
+
.filter(e -> packagePrefix(e.getKey().substring(0, e.getKey().indexOf(':'))).equals(prefix)
|
|
88
|
+
|| e.getKey().startsWith(prefix + ":"))
|
|
89
|
+
.map(Map.Entry::getValue)
|
|
90
|
+
.findFirst().orElse("unknown");
|
|
91
|
+
String category;
|
|
92
|
+
if (RUNTIME_PREFIXES.contains(prefix)) {
|
|
93
|
+
category = "runtime";
|
|
94
|
+
} else if (DEVTOOL_PREFIXES.contains(prefix)) {
|
|
95
|
+
category = "devtool";
|
|
96
|
+
} else {
|
|
97
|
+
category = "unknown";
|
|
98
|
+
}
|
|
99
|
+
Map<String, Object> meta = new LinkedHashMap<>();
|
|
100
|
+
meta.put("isThirdParty", true);
|
|
101
|
+
meta.put("packageVersion", version);
|
|
102
|
+
meta.put("category", category);
|
|
103
|
+
return meta;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** [mvn]/prefix node — null when the prefix is not permitted. */
|
|
107
|
+
public Map<String, Object> packageNode(String prefix) {
|
|
108
|
+
if (!permitted(prefix)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
String id = "[mvn]/" + prefix;
|
|
112
|
+
if (!nodes.containsKey(id)) {
|
|
113
|
+
nodes.put(id, Contract.thirdPartyNode(id, prefix, baseMetadata(prefix)));
|
|
114
|
+
}
|
|
115
|
+
return nodes.get(id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** [mvn]/prefix::Member — null when not permitted. */
|
|
119
|
+
public Map<String, Object> methodNode(String prefix, String member) {
|
|
120
|
+
if (!permitted(prefix)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
String id = "[mvn]/" + prefix + "::" + member;
|
|
124
|
+
if (!nodes.containsKey(id)) {
|
|
125
|
+
Map<String, Object> pkgNode = packageNode(prefix);
|
|
126
|
+
if (pkgNode == null) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
Map<String, Object> meta = new LinkedHashMap<>(baseMetadata(prefix));
|
|
130
|
+
meta.put("parentPackageId", pkgNode.get("id"));
|
|
131
|
+
meta.put("methodName", member);
|
|
132
|
+
nodes.put(id, Contract.thirdPartyNode(id, prefix + "." + member, meta));
|
|
133
|
+
}
|
|
134
|
+
return nodes.get(id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** All third-party nodes created so far (append AFTER edge resolution). */
|
|
138
|
+
public List<Map<String, Object>> allNodes() {
|
|
139
|
+
return new ArrayList<>(nodes.values());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package devlens.extractor;
|
|
2
|
+
|
|
3
|
+
import com.github.javaparser.ParserConfiguration;
|
|
4
|
+
import com.github.javaparser.StaticJavaParser;
|
|
5
|
+
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
|
|
6
|
+
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver;
|
|
7
|
+
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver;
|
|
8
|
+
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
|
|
9
|
+
|
|
10
|
+
import java.nio.file.Path;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* TypeSolver setup (V1 — project sources + JDK reflection only).
|
|
14
|
+
*
|
|
15
|
+
* ReflectionTypeSolver resolves JDK types (java.*, javax.*). JavaParserTypeSolver
|
|
16
|
+
* resolves types declared inside the analyzed repo (src/main/java + src/test/java).
|
|
17
|
+
*
|
|
18
|
+
* External dependency jars are deliberately NOT on the classpath in V1: Spring
|
|
19
|
+
* and other third-party types stay unresolved (Optional.empty()). Edge detectors
|
|
20
|
+
* degrade to name-based resolution for those — route detection is
|
|
21
|
+
* annotation-driven and doesn't need type resolution at all.
|
|
22
|
+
*/
|
|
23
|
+
public final class TypeSolverFactory {
|
|
24
|
+
|
|
25
|
+
private TypeSolverFactory() {}
|
|
26
|
+
|
|
27
|
+
public static void configure(Path repoRoot) {
|
|
28
|
+
CombinedTypeSolver combined = new CombinedTypeSolver();
|
|
29
|
+
combined.add(new ReflectionTypeSolver());
|
|
30
|
+
for (Path root : SourceWalker.sourceRoots(repoRoot)) {
|
|
31
|
+
combined.add(new JavaParserTypeSolver(root));
|
|
32
|
+
}
|
|
33
|
+
// JavaParser 3.26+ removed StaticJavaParser.setSymbolSolver — the
|
|
34
|
+
// resolver is configured via ParserConfiguration instead.
|
|
35
|
+
// JAVA_21 level is REQUIRED: the default level (<14) makes records
|
|
36
|
+
// and sealed types silently parse to EMPTY (non-fatal problem, zero
|
|
37
|
+
// nodes, no error recorded) — the plain fixture caught this.
|
|
38
|
+
ParserConfiguration config = new ParserConfiguration()
|
|
39
|
+
.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21)
|
|
40
|
+
.setSymbolResolver(new JavaSymbolSolver(combined));
|
|
41
|
+
StaticJavaParser.setConfiguration(config);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
package devlens.extractor.edges;
|
|
2
|
+
|
|
3
|
+
import devlens.extractor.Contract;
|
|
4
|
+
import devlens.extractor.LookupMaps;
|
|
5
|
+
import devlens.extractor.Parser;
|
|
6
|
+
import devlens.extractor.ThirdParty;
|
|
7
|
+
|
|
8
|
+
import java.util.ArrayList;
|
|
9
|
+
import java.util.LinkedHashMap;
|
|
10
|
+
import java.util.List;
|
|
11
|
+
import java.util.Map;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CALLS edge resolution (mirrors edges/calls.py — the resolution ladder).
|
|
15
|
+
*
|
|
16
|
+
* Per call fact, in order:
|
|
17
|
+
* 1. Symbol-solver target (recorded at parse time): project source hit →
|
|
18
|
+
* exact METHOD edge; JDK hit → skip (builtin tier).
|
|
19
|
+
* 2. Name-based fallback (unresolved calls):
|
|
20
|
+
* a. this./plain → same-class method
|
|
21
|
+
* b. receiver is a FIELD of the owning class → field type → class node
|
|
22
|
+
* → method lookup (also params, step c)
|
|
23
|
+
* c. receiver is a PARAM → param type → class → method
|
|
24
|
+
* d. receiver alias in the file's symbolMap → [mvn] member node (chain
|
|
25
|
+
* rule: named-import root returns the alias itself) or local class
|
|
26
|
+
* e. local variables / dynamic → unresolvable (documented parity tier)
|
|
27
|
+
* Everything unresolved stays in metadata.calls (string form) — never lost.
|
|
28
|
+
*/
|
|
29
|
+
public final class Calls {
|
|
30
|
+
|
|
31
|
+
private Calls() {}
|
|
32
|
+
|
|
33
|
+
public static void resolve(LookupMaps lookup, List<Map<String, Object>> edgesOut) {
|
|
34
|
+
for (Map.Entry<String, List<Parser.CallInfo>> entry : lookup.methodCallFacts.entrySet()) {
|
|
35
|
+
String methodNodeId = entry.getKey();
|
|
36
|
+
Map<String, Object> methodNode = lookup.nodeById.get(methodNodeId);
|
|
37
|
+
if (methodNode == null) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
Map<String, Object> metadata = (Map<String, Object>) methodNode.get("metadata");
|
|
41
|
+
String relPath = (String) methodNode.get("filePath");
|
|
42
|
+
String parentClass = (String) metadata.get("parentClass");
|
|
43
|
+
String pkg = packageOf(methodNodeId, relPath);
|
|
44
|
+
List<String> resolvedCalls = new ArrayList<>();
|
|
45
|
+
List<Map<String, String>> resolvedDetails = new ArrayList<>();
|
|
46
|
+
|
|
47
|
+
for (Parser.CallInfo ci : entry.getValue()) {
|
|
48
|
+
String target = resolveOne(ci, lookup, relPath, parentClass, pkg, methodNodeId);
|
|
49
|
+
if (target != null) {
|
|
50
|
+
Map<String, Object> edge = Contract.edge("CALLS", methodNodeId, target,
|
|
51
|
+
Map.of("line", ci.line));
|
|
52
|
+
edgesOut.add(edge);
|
|
53
|
+
resolvedCalls.add(target);
|
|
54
|
+
// details for OrmEdges (repo R/W classification)
|
|
55
|
+
Map<String, String> detail = new LinkedHashMap<>();
|
|
56
|
+
detail.put("target", target);
|
|
57
|
+
detail.put("method", ci.methodName);
|
|
58
|
+
detail.put("class", classNodeOf(target));
|
|
59
|
+
resolvedDetails.add(detail);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!resolvedCalls.isEmpty()) {
|
|
63
|
+
java.util.Collections.sort(resolvedCalls);
|
|
64
|
+
metadata.put("resolvedCalls", resolvedCalls);
|
|
65
|
+
metadata.put("resolvedCallDetails", resolvedDetails);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static String resolveOne(Parser.CallInfo ci, LookupMaps lookup,
|
|
71
|
+
String relPath, String parentClass,
|
|
72
|
+
String pkg, String methodNodeId) {
|
|
73
|
+
// 1. symbol-solver target
|
|
74
|
+
if (ci.resolvedTarget != null) {
|
|
75
|
+
int lastDot = ci.resolvedTarget.lastIndexOf('.');
|
|
76
|
+
String classFqcn = ci.resolvedTarget.substring(0, lastDot);
|
|
77
|
+
String method = ci.resolvedTarget.substring(lastDot + 1);
|
|
78
|
+
if (ThirdParty.isJdk(classFqcn)) {
|
|
79
|
+
return null; // builtin tier
|
|
80
|
+
}
|
|
81
|
+
String rel = lookup.typeMap.get(classFqcn);
|
|
82
|
+
if (rel != null) {
|
|
83
|
+
String dotted = lookup.typeDottedMap.get(classFqcn);
|
|
84
|
+
String classId = rel + "::" + dotted;
|
|
85
|
+
String methodId = classId + "." + method;
|
|
86
|
+
if (lookup.nodeById.containsKey(methodId)) {
|
|
87
|
+
return methodId;
|
|
88
|
+
}
|
|
89
|
+
// constructor call resolved via class — no method node for <init> chains
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// external (no jars on classpath in V1) → fall through to name-based
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
String receiver = ci.receiverName;
|
|
96
|
+
// 2a. this./plain call → same-class method
|
|
97
|
+
if (receiver == null || "this".equals(receiver) || "super".equals(receiver)) {
|
|
98
|
+
return sameClassMethod(methodNodeId, parentClass, ci.methodName, lookup);
|
|
99
|
+
}
|
|
100
|
+
// 2d. alias in symbolMap (imported type or third-party member)
|
|
101
|
+
String aliasTarget = lookup.symbolMap(relPath).get(receiver);
|
|
102
|
+
if (aliasTarget != null) {
|
|
103
|
+
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;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// 2b/2c. receiver is a field or param of the owning method's class
|
|
113
|
+
String receiverType = fieldOrParamType(methodNodeId, lookup, receiver);
|
|
114
|
+
if (receiverType != null && !receiverType.isEmpty()) {
|
|
115
|
+
String classId = resolveTypeRef(receiverType, lookup, relPath, pkg);
|
|
116
|
+
if (classId != null) {
|
|
117
|
+
if (classId.startsWith("[mvn]")) {
|
|
118
|
+
return classId;
|
|
119
|
+
}
|
|
120
|
+
String methodId = classId + "." + ci.methodName;
|
|
121
|
+
if (lookup.nodeById.containsKey(methodId)) {
|
|
122
|
+
return methodId;
|
|
123
|
+
}
|
|
124
|
+
// Spring Data repos inherit findAll/save/findById/... — not
|
|
125
|
+
// declared in the source file. Edge to the repo interface
|
|
126
|
+
// itself (OrmEdges turns these into R/W edges by method name).
|
|
127
|
+
Map<String, Object> classNode = lookup.nodeById.get(classId);
|
|
128
|
+
if (classNode != null) {
|
|
129
|
+
Map<String, Object> cm = (Map<String, Object>) classNode.get("metadata");
|
|
130
|
+
if (Boolean.TRUE.equals(cm.get("isRepository"))) {
|
|
131
|
+
return classId;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 2e. local vars / dynamic → unresolvable
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Method on the SAME class (or inherited — same file). */
|
|
142
|
+
private static String sameClassMethod(String methodNodeId, String parentClass,
|
|
143
|
+
String methodName, LookupMaps lookup) {
|
|
144
|
+
int colon = methodNodeId.lastIndexOf("::");
|
|
145
|
+
String prefix = colon < 0 ? methodNodeId : methodNodeId.substring(0, colon);
|
|
146
|
+
String id = prefix + "::" + parentClass + "." + methodName;
|
|
147
|
+
if (lookup.nodeById.containsKey(id)) {
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Field or param type of the method's owning class. Scans the owning
|
|
155
|
+
* CLASS node's metadata.fields + this method's metadata.params.
|
|
156
|
+
*/
|
|
157
|
+
private static String fieldOrParamType(String methodNodeId, LookupMaps lookup, String receiver) {
|
|
158
|
+
if (receiver == null) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
Map<String, Object> methodNode = lookup.nodeById.get(methodNodeId);
|
|
162
|
+
if (methodNode == null) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
Map<String, Object> mmeta = (Map<String, Object>) methodNode.get("metadata");
|
|
166
|
+
// params: ["Type name", ...]
|
|
167
|
+
@SuppressWarnings("unchecked")
|
|
168
|
+
List<String> params = (List<String>) mmeta.get("params");
|
|
169
|
+
if (params != null) {
|
|
170
|
+
for (String p : params) {
|
|
171
|
+
int sp = p.indexOf(' ');
|
|
172
|
+
String pname = sp < 0 ? p : p.substring(sp + 1).trim();
|
|
173
|
+
if (pname.equals(receiver)) {
|
|
174
|
+
return Parser.simpleTypeName(p);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
String parentClass = (String) mmeta.get("parentClass");
|
|
179
|
+
String relPath = (String) methodNode.get("filePath");
|
|
180
|
+
Map<String, String> byFile = lookup.nodesByFile.get(relPath);
|
|
181
|
+
if (byFile == null) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
String classId = byFile.get(parentClass);
|
|
185
|
+
if (classId == null) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
Map<String, Object> classNode = lookup.nodeById.get(classId);
|
|
189
|
+
if (classNode == null) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
@SuppressWarnings("unchecked")
|
|
193
|
+
List<Map<String, Object>> fields =
|
|
194
|
+
(List<Map<String, Object>>) ((Map<String, Object>) classNode.get("metadata")).get("fields");
|
|
195
|
+
if (fields != null) {
|
|
196
|
+
for (Map<String, Object> f : fields) {
|
|
197
|
+
if (receiver.equals(f.get("name"))) {
|
|
198
|
+
return (String) f.get("type");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve a simple type name to a node id: symbolMap first, then
|
|
207
|
+
* same-package FQCN, then closest-by-name across the project.
|
|
208
|
+
*/
|
|
209
|
+
public static String resolveTypeRef(String simpleName, LookupMaps lookup,
|
|
210
|
+
String callerRelPath, String callerPkg) {
|
|
211
|
+
if (simpleName == null || simpleName.isEmpty()) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
String alias = lookup.symbolMap(callerRelPath).get(simpleName);
|
|
215
|
+
if (alias != null) {
|
|
216
|
+
return alias;
|
|
217
|
+
}
|
|
218
|
+
if (callerPkg != null && !callerPkg.isEmpty()) {
|
|
219
|
+
String fqcn = callerPkg + "." + simpleName;
|
|
220
|
+
String rel = lookup.typeMap.get(fqcn);
|
|
221
|
+
if (rel != null) {
|
|
222
|
+
return rel + "::" + lookup.typeDottedMap.get(fqcn);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
String closest = lookup.closestByName(simpleName, callerRelPath);
|
|
226
|
+
if (closest == null) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (closest.startsWith("[mvn]")) {
|
|
230
|
+
return closest;
|
|
231
|
+
}
|
|
232
|
+
Map<String, Object> node = lookup.nodeById.get(closest);
|
|
233
|
+
if (node == null) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
String type = (String) node.get("type");
|
|
237
|
+
return ("CLASS".equals(type) || "INTERFACE".equals(type) || "ENUM".equals(type))
|
|
238
|
+
? closest : null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Package of the file containing a node id. */
|
|
242
|
+
private static String packageOf(String nodeId, String relPath) {
|
|
243
|
+
// derive from the file path minus src/main/java|src/test/java prefix
|
|
244
|
+
String p = relPath;
|
|
245
|
+
for (String root : new String[]{"src/main/java/", "src/test/java/"}) {
|
|
246
|
+
if (p.startsWith(root)) {
|
|
247
|
+
p = p.substring(root.length());
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (p.endsWith(".java")) {
|
|
252
|
+
p = p.substring(0, p.length() - 5);
|
|
253
|
+
}
|
|
254
|
+
return p.replace('/', '.');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Class node id for a method node id (strip the trailing .method). */
|
|
258
|
+
private static String classNodeOf(String methodId) {
|
|
259
|
+
int lastDot = methodId.lastIndexOf('.');
|
|
260
|
+
int colon = methodId.lastIndexOf("::");
|
|
261
|
+
if (lastDot > colon && colon >= 0) {
|
|
262
|
+
return methodId.substring(0, lastDot);
|
|
263
|
+
}
|
|
264
|
+
return methodId;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package devlens.extractor.edges;
|
|
2
|
+
|
|
3
|
+
import devlens.extractor.LookupMaps;
|
|
4
|
+
|
|
5
|
+
import java.util.List;
|
|
6
|
+
import java.util.Map;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Semantic metadata enrichment (mirrors edges/enrich.py).
|
|
10
|
+
*
|
|
11
|
+
* Pure metadata — cheap annotation scans on already-collected facts (no AST
|
|
12
|
+
* re-walks, so the Python "memo isolation" pitfall does not apply here:
|
|
13
|
+
* every scan reads a fresh copy of node metadata).
|
|
14
|
+
*/
|
|
15
|
+
public final class Enrich {
|
|
16
|
+
|
|
17
|
+
private Enrich() {}
|
|
18
|
+
|
|
19
|
+
public static void enrich(LookupMaps lookup) {
|
|
20
|
+
for (Map<String, Object> node : lookup.nodeById.values()) {
|
|
21
|
+
String type = (String) node.get("type");
|
|
22
|
+
Map<String, Object> meta = (Map<String, Object>) node.get("metadata");
|
|
23
|
+
if (meta == null) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
@SuppressWarnings("unchecked")
|
|
27
|
+
List<String> annotations = (List<String>) meta.get("annotations");
|
|
28
|
+
if (annotations == null) {
|
|
29
|
+
annotations = List.of();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if ("CLASS".equals(type)) {
|
|
33
|
+
if (annotations.contains("RestController") || annotations.contains("Controller")) {
|
|
34
|
+
meta.put("isController", true);
|
|
35
|
+
}
|
|
36
|
+
if (annotations.contains("Service")) {
|
|
37
|
+
meta.put("isService", true);
|
|
38
|
+
}
|
|
39
|
+
if (annotations.contains("Configuration") || annotations.contains("SpringBootApplication")) {
|
|
40
|
+
meta.put("isConfig", true);
|
|
41
|
+
}
|
|
42
|
+
if (annotations.contains("Component")) {
|
|
43
|
+
meta.put("isComponent", true);
|
|
44
|
+
}
|
|
45
|
+
if ("record".equals(meta.get("kind"))) {
|
|
46
|
+
meta.put("isSchema", true); // records are DTO/value conventions
|
|
47
|
+
}
|
|
48
|
+
if (annotations.contains("RestControllerAdvice") || annotations.contains("ControllerAdvice")) {
|
|
49
|
+
meta.put("isExceptionHandler", true);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package devlens.extractor.edges;
|
|
2
|
+
|
|
3
|
+
import devlens.extractor.Contract;
|
|
4
|
+
import devlens.extractor.LookupMaps;
|
|
5
|
+
import devlens.extractor.Parser;
|
|
6
|
+
import devlens.extractor.ThirdParty;
|
|
7
|
+
|
|
8
|
+
import java.util.ArrayList;
|
|
9
|
+
import java.util.List;
|
|
10
|
+
import java.util.Map;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* IMPORTS edge resolution (mirrors edges/imports.py).
|
|
14
|
+
*
|
|
15
|
+
* Java imports are fully qualified → resolution is exact via typeMap:
|
|
16
|
+
* import com.foo.bar.Baz → typeMap["com.foo.bar.Baz"] → local file edge.
|
|
17
|
+
* Wildcards expand to the types the file ACTUALLY uses (usedTypeNames),
|
|
18
|
+
* static imports resolve to their containing class. JDK → nothing. External
|
|
19
|
+
* non-JDK → gated [mvn] nodes + the import alias lands in the file's
|
|
20
|
+
* symbolMap (the bridge Calls/Inheritance/Tests consume).
|
|
21
|
+
*/
|
|
22
|
+
public final class Imports {
|
|
23
|
+
|
|
24
|
+
private Imports() {}
|
|
25
|
+
|
|
26
|
+
public static void resolve(List<Parser.ParsedFile> files, LookupMaps lookup,
|
|
27
|
+
ThirdParty tp, List<Map<String, Object>> edgesOut) {
|
|
28
|
+
for (Parser.ParsedFile pf : files) {
|
|
29
|
+
Map<String, String> sm = lookup.symbolMap(pf.relPath);
|
|
30
|
+
List<String> imported = new ArrayList<>();
|
|
31
|
+
for (Parser.ImportInfo imp : pf.imports) {
|
|
32
|
+
if (imp.isStatic) {
|
|
33
|
+
resolveStatic(pf, imp, lookup, sm, edgesOut, imported);
|
|
34
|
+
} else if (imp.isWildcard) {
|
|
35
|
+
resolveWildcard(pf, imp, lookup, tp, sm, edgesOut, imported);
|
|
36
|
+
} else {
|
|
37
|
+
resolveSingle(pf, imp, lookup, tp, sm, edgesOut, imported);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
java.util.Collections.sort(imported);
|
|
41
|
+
attachImportsMetadata(lookup, pf, imported);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static void resolveSingle(Parser.ParsedFile pf, Parser.ImportInfo imp,
|
|
46
|
+
LookupMaps lookup, ThirdParty tp,
|
|
47
|
+
Map<String, String> sm,
|
|
48
|
+
List<Map<String, Object>> edgesOut,
|
|
49
|
+
List<String> imported) {
|
|
50
|
+
String fqcn = imp.name;
|
|
51
|
+
String simple = simpleName(fqcn);
|
|
52
|
+
String rel = lookup.typeMap.get(fqcn);
|
|
53
|
+
if (rel != null) {
|
|
54
|
+
String dotted = lookup.typeDottedMap.get(fqcn);
|
|
55
|
+
String targetId = rel + "::" + dotted;
|
|
56
|
+
edgesOut.add(Contract.edge("IMPORTS", "file::" + pf.relPath, "file::" + rel, null));
|
|
57
|
+
sm.put(simple, targetId);
|
|
58
|
+
imported.add(rel);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (ThirdParty.isJdk(fqcn)) {
|
|
62
|
+
return; // stdlib tier — no node, no edge
|
|
63
|
+
}
|
|
64
|
+
String prefix = ThirdParty.packagePrefix(fqcn);
|
|
65
|
+
Map<String, Object> pkgNode = tp.packageNode(prefix);
|
|
66
|
+
if (pkgNode == null) {
|
|
67
|
+
return; // gated out
|
|
68
|
+
}
|
|
69
|
+
Map<String, Object> memberNode = tp.methodNode(prefix, simple);
|
|
70
|
+
String targetId = (String) memberNode.get("id");
|
|
71
|
+
edgesOut.add(Contract.edge("IMPORTS", "file::" + pf.relPath, targetId, null));
|
|
72
|
+
sm.put(simple, targetId);
|
|
73
|
+
imported.add(targetId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wildcard: import com.foo.* — resolve only the simple names this file
|
|
78
|
+
* actually uses (type refs, annotations, field/param types). Local types
|
|
79
|
+
* become per-type edges; external packages become a single package node.
|
|
80
|
+
*/
|
|
81
|
+
private static void resolveWildcard(Parser.ParsedFile pf, Parser.ImportInfo imp,
|
|
82
|
+
LookupMaps lookup, ThirdParty tp,
|
|
83
|
+
Map<String, String> sm,
|
|
84
|
+
List<Map<String, Object>> edgesOut,
|
|
85
|
+
List<String> imported) {
|
|
86
|
+
String prefix = imp.name; // com.foo
|
|
87
|
+
if (ThirdParty.isJdk(prefix)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
boolean anyLocal = false;
|
|
91
|
+
for (String used : pf.usedTypeNames) {
|
|
92
|
+
if (used == null || used.isEmpty()) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
String fqcn = prefix + "." + used;
|
|
96
|
+
String rel = lookup.typeMap.get(fqcn);
|
|
97
|
+
if (rel != null) {
|
|
98
|
+
String dotted = lookup.typeDottedMap.get(fqcn);
|
|
99
|
+
edgesOut.add(Contract.edge("IMPORTS", "file::" + pf.relPath, "file::" + rel, null));
|
|
100
|
+
sm.put(used, rel + "::" + dotted);
|
|
101
|
+
imported.add(rel);
|
|
102
|
+
anyLocal = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!anyLocal) {
|
|
106
|
+
Map<String, Object> pkgNode = tp.packageNode(ThirdParty.packagePrefix(prefix));
|
|
107
|
+
if (pkgNode != null) {
|
|
108
|
+
edgesOut.add(Contract.edge("IMPORTS", "file::" + pf.relPath,
|
|
109
|
+
(String) pkgNode.get("id"), null));
|
|
110
|
+
imported.add((String) pkgNode.get("id"));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** import static com.foo.Constants.PI → resolve the containing class. */
|
|
116
|
+
private static void resolveStatic(Parser.ParsedFile pf, Parser.ImportInfo imp,
|
|
117
|
+
LookupMaps lookup, Map<String, String> sm,
|
|
118
|
+
List<Map<String, Object>> edgesOut,
|
|
119
|
+
List<String> imported) {
|
|
120
|
+
String fqcn = imp.name; // com.foo.Constants (member dropped for V1)
|
|
121
|
+
String rel = lookup.typeMap.get(fqcn);
|
|
122
|
+
if (rel != null) {
|
|
123
|
+
String dotted = lookup.typeDottedMap.get(fqcn);
|
|
124
|
+
edgesOut.add(Contract.edge("IMPORTS", "file::" + pf.relPath, "file::" + rel, null));
|
|
125
|
+
sm.put(simpleName(fqcn), rel + "::" + dotted);
|
|
126
|
+
imported.add(rel);
|
|
127
|
+
}
|
|
128
|
+
// external static imports (constants from libs) → skip for V1
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** metadata.imports = sorted resolved targets, on every node of the file. */
|
|
132
|
+
private static void attachImportsMetadata(LookupMaps lookup, Parser.ParsedFile pf,
|
|
133
|
+
List<String> imported) {
|
|
134
|
+
Map<String, Object> fileNode = lookup.fileNodesByPath.get(pf.relPath);
|
|
135
|
+
if (fileNode != null) {
|
|
136
|
+
((Map<String, Object>) fileNode.get("metadata")).put("imports", imported);
|
|
137
|
+
}
|
|
138
|
+
Map<String, String> byFile = lookup.nodesByFile.get(pf.relPath);
|
|
139
|
+
if (byFile == null) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (String id : byFile.values()) {
|
|
143
|
+
Map<String, Object> node = lookup.nodeById.get(id);
|
|
144
|
+
if (node != null) {
|
|
145
|
+
((Map<String, Object>) node.get("metadata")).put("imports", imported);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static String simpleName(String fqcn) {
|
|
151
|
+
int dot = fqcn.lastIndexOf('.');
|
|
152
|
+
return dot < 0 ? fqcn : fqcn.substring(dot + 1);
|
|
153
|
+
}
|
|
154
|
+
}
|