devlensio 0.6.2 → 1.0.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.
Files changed (129) hide show
  1. package/README.md +89 -31
  2. package/dist/extractors/detectLanguage.d.ts +2 -0
  3. package/dist/extractors/detectLanguage.js +33 -0
  4. package/dist/extractors/index.d.ts +6 -0
  5. package/dist/extractors/index.js +116 -0
  6. package/dist/extractors/runner.d.ts +7 -0
  7. package/dist/extractors/runner.js +194 -0
  8. package/dist/extractors/types.d.ts +37 -0
  9. package/dist/extractors/types.js +8 -0
  10. package/dist/graph/buildLookup.d.ts +1 -0
  11. package/dist/graph/buildLookup.js +2 -1
  12. package/dist/graph/edges/callEdges.js +19 -5
  13. package/dist/graph/edges/callEdges.test.d.ts +1 -0
  14. package/dist/graph/edges/callEdges.test.js +200 -0
  15. package/dist/graph/edges/importEdges.js +12 -0
  16. package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
  17. package/dist/graph/edges/inheritanceEdges.js +73 -0
  18. package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
  19. package/dist/graph/edges/inheritanceEdges.test.js +140 -0
  20. package/dist/graph/index.js +4 -0
  21. package/dist/parser/classes.test.d.ts +1 -0
  22. package/dist/parser/classes.test.js +360 -0
  23. package/dist/parser/extractors/classes.d.ts +5 -0
  24. package/dist/parser/extractors/classes.js +241 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +7 -1
  27. package/dist/pipeline/index.d.ts +2 -1
  28. package/dist/pipeline/index.js +30 -42
  29. package/dist/scoring/index.js +8 -0
  30. package/dist/scoring/index.test.js +43 -0
  31. package/dist/scoring/nodeScorer.js +2 -0
  32. package/dist/scoring/pruneDisconnected.d.ts +8 -0
  33. package/dist/scoring/pruneDisconnected.js +66 -0
  34. package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
  35. package/dist/scoring/pruneDisconnected.test.js +123 -0
  36. package/dist/summarizer/prompts.d.ts +1 -1
  37. package/dist/types.d.ts +5 -5
  38. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  39. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  40. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  41. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  42. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  43. package/extractors/go/build.mjs +43 -0
  44. package/extractors/go/calls.go +289 -0
  45. package/extractors/go/contract.go +140 -0
  46. package/extractors/go/extractor.go +161 -0
  47. package/extractors/go/fingerprint.go +167 -0
  48. package/extractors/go/go.mod +3 -0
  49. package/extractors/go/imports.go +90 -0
  50. package/extractors/go/inheritance.go +138 -0
  51. package/extractors/go/lookup.go +106 -0
  52. package/extractors/go/main.go +57 -0
  53. package/extractors/go/nodes.go +177 -0
  54. package/extractors/go/orm_edges.go +277 -0
  55. package/extractors/go/parser.go +469 -0
  56. package/extractors/go/routes.go +521 -0
  57. package/extractors/go/tests.go +45 -0
  58. package/extractors/go/thirdparty.go +138 -0
  59. package/extractors/go/typeload.go +178 -0
  60. package/extractors/go/walker.go +67 -0
  61. package/extractors/java/build.mjs +90 -0
  62. package/extractors/java/devlens_java_extractor.jar +0 -0
  63. package/extractors/java/src/devlens/extractor/Contract.java +171 -0
  64. package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
  65. package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
  66. package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
  67. package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
  68. package/extractors/java/src/devlens/extractor/Main.java +66 -0
  69. package/extractors/java/src/devlens/extractor/Parser.java +522 -0
  70. package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
  71. package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
  72. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
  73. package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
  74. package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
  75. package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
  76. package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
  77. package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
  78. package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
  79. package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
  80. package/extractors/python/devlens_extractors_python/__init__.py +3 -0
  81. package/extractors/python/devlens_extractors_python/__main__.py +33 -0
  82. package/extractors/python/devlens_extractors_python/contract.py +94 -0
  83. package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
  84. package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
  85. package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
  86. package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
  87. package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
  88. package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
  89. package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
  90. package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
  91. package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
  92. package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
  93. package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
  94. package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
  95. package/extractors/python/devlens_extractors_python/extractor.py +107 -0
  96. package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
  97. package/extractors/python/devlens_extractors_python/lookup.py +72 -0
  98. package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
  99. package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
  100. package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
  101. package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
  102. package/extractors/python/devlens_extractors_python/third_party.py +103 -0
  103. package/extractors/python/pyproject.toml +16 -0
  104. package/extractors/python/setup.mjs +54 -0
  105. package/extractors/rust/Cargo.toml +29 -0
  106. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  107. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  108. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  109. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  110. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  111. package/extractors/rust/build.mjs +85 -0
  112. package/extractors/rust/src/calls.rs +295 -0
  113. package/extractors/rust/src/contract.rs +236 -0
  114. package/extractors/rust/src/enrich.rs +107 -0
  115. package/extractors/rust/src/extractor.rs +199 -0
  116. package/extractors/rust/src/fingerprint.rs +238 -0
  117. package/extractors/rust/src/imports.rs +101 -0
  118. package/extractors/rust/src/inheritance.rs +233 -0
  119. package/extractors/rust/src/lookup.rs +226 -0
  120. package/extractors/rust/src/main.rs +53 -0
  121. package/extractors/rust/src/module_map.rs +131 -0
  122. package/extractors/rust/src/nodes.rs +174 -0
  123. package/extractors/rust/src/orm_edges.rs +125 -0
  124. package/extractors/rust/src/parser.rs +859 -0
  125. package/extractors/rust/src/routes.rs +1265 -0
  126. package/extractors/rust/src/tests.rs +109 -0
  127. package/extractors/rust/src/thirdparty.rs +114 -0
  128. package/extractors/rust/src/walker.rs +82 -0
  129. 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
+ }