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.
- 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 +116 -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,79 @@
|
|
|
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.List;
|
|
9
|
+
import java.util.Map;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* EXTENDS / IMPLEMENTS edge resolution (mirrors edges/inheritance.py).
|
|
13
|
+
*
|
|
14
|
+
* Java is the EASIEST of all languages here — explicit keywords, zero
|
|
15
|
+
* heuristics:
|
|
16
|
+
* extends Baz → EXTENDS (always)
|
|
17
|
+
* implements A, B → IMPLEMENTS (always; interfaces extending interfaces
|
|
18
|
+
* also use extends → EXTENDS)
|
|
19
|
+
*
|
|
20
|
+
* Targets resolve via symbol maps → same-package → closest-by-name. Local
|
|
21
|
+
* bases become class edges; gated third-party bases become [mvn] edges;
|
|
22
|
+
* unresolved bases are skipped — NEVER a dangling edge.
|
|
23
|
+
*/
|
|
24
|
+
public final class Inheritance {
|
|
25
|
+
|
|
26
|
+
private Inheritance() {}
|
|
27
|
+
|
|
28
|
+
public static void resolve(List<Parser.ParsedFile> files, LookupMaps lookup,
|
|
29
|
+
ThirdParty tp, List<Map<String, Object>> edgesOut) {
|
|
30
|
+
for (Parser.ParsedFile pf : files) {
|
|
31
|
+
if (pf.isTest) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
String pkg = pf.packageName;
|
|
35
|
+
for (Parser.TypeInfo t : pf.allTypes()) {
|
|
36
|
+
String nodeId = pf.relPath + "::" + t.dottedName;
|
|
37
|
+
if (t.extendedType != null && !t.extendedType.isEmpty()) {
|
|
38
|
+
String target = resolveBase(t.extendedType, pf, lookup, tp);
|
|
39
|
+
if (target != null) {
|
|
40
|
+
edgesOut.add(Contract.edge("EXTENDS", nodeId, target, null));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (String impl : t.implementedTypes) {
|
|
44
|
+
String target = resolveBase(impl, pf, lookup, tp);
|
|
45
|
+
if (target != null) {
|
|
46
|
+
edgesOut.add(Contract.edge("IMPLEMENTS", nodeId, target, null));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private static String resolveBase(String simpleName, Parser.ParsedFile pf,
|
|
54
|
+
LookupMaps lookup, ThirdParty tp) {
|
|
55
|
+
String alias = lookup.symbolMap(pf.relPath).get(simpleName);
|
|
56
|
+
if (alias != null) {
|
|
57
|
+
// [mvn] aliases only exist when gated in (Imports checked `permitted`)
|
|
58
|
+
return alias;
|
|
59
|
+
}
|
|
60
|
+
if (!pf.packageName.isEmpty()) {
|
|
61
|
+
String fqcn = pf.packageName + "." + simpleName;
|
|
62
|
+
String rel = lookup.typeMap.get(fqcn);
|
|
63
|
+
if (rel != null) {
|
|
64
|
+
return rel + "::" + lookup.typeDottedMap.get(fqcn);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
String closest = lookup.closestByName(simpleName, pf.relPath);
|
|
68
|
+
if (closest != null) {
|
|
69
|
+
Map<String, Object> node = lookup.nodeById.get(closest);
|
|
70
|
+
if (node != null) {
|
|
71
|
+
String type = (String) node.get("type");
|
|
72
|
+
if ("CLASS".equals(type) || "INTERFACE".equals(type) || "ENUM".equals(type)) {
|
|
73
|
+
return closest;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null; // external/unresolved — skip, never dangling
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
package devlens.extractor.edges;
|
|
2
|
+
|
|
3
|
+
import devlens.extractor.Contract;
|
|
4
|
+
import devlens.extractor.LookupMaps;
|
|
5
|
+
import devlens.extractor.Parser;
|
|
6
|
+
|
|
7
|
+
import java.util.ArrayList;
|
|
8
|
+
import java.util.LinkedHashMap;
|
|
9
|
+
import java.util.List;
|
|
10
|
+
import java.util.Map;
|
|
11
|
+
import java.util.regex.Matcher;
|
|
12
|
+
import java.util.regex.Pattern;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* JPA data layer (mirrors edges/orm_edges.py — highest fidelity of all
|
|
16
|
+
* languages per the playbook).
|
|
17
|
+
*
|
|
18
|
+
* Models: @Entity/@Table/@MappedSuperclass/@Embeddable → isModel
|
|
19
|
+
* (type "jpa"); @Document → "mongodb". fields[] from the class.
|
|
20
|
+
* Repos: interface extends JpaRepository<Entity, ID> etc → isRepository
|
|
21
|
+
* + linkedModel = the entity generic arg (via rawCode regex).
|
|
22
|
+
* R/W edges: consumer → store, same direction as stateEdges.ts —
|
|
23
|
+
* - repo's own derived methods (findByX, save, deleteByX) → R/W to entity
|
|
24
|
+
* - calls INTO repo methods (from services/controllers) → R/W to entity
|
|
25
|
+
* method-name grammar classifies: find|get|query|read|count|exists|select
|
|
26
|
+
* = READ; save|insert|update|delete|remove|persist = WRITE.
|
|
27
|
+
*/
|
|
28
|
+
public final class OrmEdges {
|
|
29
|
+
|
|
30
|
+
private static final List<String> MODEL_ANNOTATIONS =
|
|
31
|
+
List.of("Entity", "Table", "MappedSuperclass", "Embeddable");
|
|
32
|
+
private static final List<String> REPO_TYPES = List.of(
|
|
33
|
+
"JpaRepository", "CrudRepository", "PagingAndSortingRepository",
|
|
34
|
+
"ListCrudRepository", "ListPagingAndSortingRepository",
|
|
35
|
+
"JpaSpecificationExecutor", "QuerydslPredicateExecutor",
|
|
36
|
+
"MongoRepository", "ReactiveCrudRepository", "R2dbcRepository",
|
|
37
|
+
"JdbcRepository", "ReactiveMongoRepository",
|
|
38
|
+
"Repository"); // Spring Data's base marker interface
|
|
39
|
+
|
|
40
|
+
private static final Pattern GENERIC_ARG =
|
|
41
|
+
Pattern.compile("extends\\s+[A-Za-z_][\\w.]*\\s*<\\s*([A-Za-z_][\\w.]*)");
|
|
42
|
+
|
|
43
|
+
private OrmEdges() {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detection pass — MUST run BEFORE Calls.resolve: model metadata
|
|
47
|
+
* (isModel/modelType/fields) + repository detection (isRepository,
|
|
48
|
+
* linkedModel) + derived-method R/W edges on repo interfaces.
|
|
49
|
+
*/
|
|
50
|
+
public static void detect(LookupMaps lookup, List<Map<String, Object>> edgesOut) {
|
|
51
|
+
// ── 1. model metadata ─────────────────────────────────────────────
|
|
52
|
+
for (Map<String, Object> node : lookup.nodeById.values()) {
|
|
53
|
+
if (!"CLASS".equals(node.get("type"))) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
Map<String, Object> meta = (Map<String, Object>) node.get("metadata");
|
|
57
|
+
@SuppressWarnings("unchecked")
|
|
58
|
+
List<String> annotations = (List<String>) meta.get("annotations");
|
|
59
|
+
if (annotations == null) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
boolean isEntity = annotations.stream().anyMatch(MODEL_ANNOTATIONS::contains);
|
|
63
|
+
boolean isDocument = annotations.contains("Document");
|
|
64
|
+
if (isEntity || isDocument) {
|
|
65
|
+
meta.put("isModel", true);
|
|
66
|
+
meta.put("modelType", isDocument ? "mongodb" : "jpa");
|
|
67
|
+
// fields[] already on metadata from node build — expose explicitly
|
|
68
|
+
meta.putIfAbsent("fields", meta.get("fields") == null
|
|
69
|
+
? new ArrayList<Map<String, Object>>() : meta.get("fields"));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── 2. repository detection + derived-method R/W edges ────────────
|
|
74
|
+
for (Map<String, Object> node : lookup.nodeById.values()) {
|
|
75
|
+
String type = (String) node.get("type");
|
|
76
|
+
if (!"INTERFACE".equals(type)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
Map<String, Object> meta = (Map<String, Object>) node.get("metadata");
|
|
80
|
+
String ext = (String) meta.get("extendsType");
|
|
81
|
+
if (ext == null || REPO_TYPES.stream().noneMatch(ext::equals)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
meta.put("isRepository", true);
|
|
85
|
+
String entityId = entityGenericArg(node, lookup);
|
|
86
|
+
if (entityId != null) {
|
|
87
|
+
meta.put("linkedModel", entityId);
|
|
88
|
+
// derived query methods on the repo itself
|
|
89
|
+
for (Map<String, Object> m : lookup.nodeById.values()) {
|
|
90
|
+
if (node.get("id").equals(parentClassId(m))) {
|
|
91
|
+
String methodName = (String) m.get("name");
|
|
92
|
+
String edgeType = readOrWrite(methodName);
|
|
93
|
+
if (edgeType != null) {
|
|
94
|
+
edgesOut.add(Contract.edge(edgeType, (String) node.get("id"), entityId,
|
|
95
|
+
Map.of("via", "repository-derived")));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Consumer pass — AFTER Calls.resolve: calls into repo methods become
|
|
105
|
+
* READS_FROM/WRITES_TO edges from the calling class to the entity.
|
|
106
|
+
*/
|
|
107
|
+
public static void consumerEdges(LookupMaps lookup, List<Map<String, Object>> edgesOut) {
|
|
108
|
+
for (Map.Entry<String, List<Parser.CallInfo>> entry : lookup.methodCallFacts.entrySet()) {
|
|
109
|
+
Map<String, Object> methodNode = lookup.nodeById.get(entry.getKey());
|
|
110
|
+
if (methodNode == null) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
@SuppressWarnings("unchecked")
|
|
114
|
+
List<Map<String, String>> details =
|
|
115
|
+
(List<Map<String, String>>) ((Map<String, Object>) methodNode.get("metadata"))
|
|
116
|
+
.get("resolvedCallDetails");
|
|
117
|
+
if (details == null) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
String ownerClassId = ownerClassId(methodNode);
|
|
121
|
+
for (Map<String, String> d : details) {
|
|
122
|
+
String targetClass = d.get("class");
|
|
123
|
+
Map<String, Object> targetNode = lookup.nodeById.get(targetClass);
|
|
124
|
+
if (targetNode == null) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
Map<String, Object> tmeta = (Map<String, Object>) targetNode.get("metadata");
|
|
128
|
+
if (!Boolean.TRUE.equals(tmeta.get("isRepository"))) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
String entityId = (String) tmeta.get("linkedModel");
|
|
132
|
+
if (entityId == null) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
String edgeType = readOrWrite(d.get("method"));
|
|
136
|
+
if (edgeType != null) {
|
|
137
|
+
edgesOut.add(Contract.edge(edgeType, ownerClassId, entityId,
|
|
138
|
+
Map.of("via", "repository-call")));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** First generic arg of the extends clause: JpaRepository<User, Long> → User. */
|
|
145
|
+
private static String entityGenericArg(Map<String, Object> repoNode, LookupMaps lookup) {
|
|
146
|
+
String rawCode = (String) repoNode.get("rawCode");
|
|
147
|
+
if (rawCode == null) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
Matcher m = GENERIC_ARG.matcher(rawCode);
|
|
151
|
+
if (!m.find()) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
String entitySimple = Parser.simpleTypeName(m.group(1));
|
|
155
|
+
String relPath = (String) repoNode.get("filePath");
|
|
156
|
+
String pkg = packageOf(relPath);
|
|
157
|
+
return Calls.resolveTypeRef(entitySimple, lookup, relPath, pkg);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private static String readOrWrite(String methodName) {
|
|
161
|
+
if (methodName == null) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
// method node names are "Class.method" — classify on the method part
|
|
165
|
+
String n = methodName.contains(".")
|
|
166
|
+
? methodName.substring(methodName.lastIndexOf('.') + 1) : methodName;
|
|
167
|
+
n = n.toLowerCase();
|
|
168
|
+
if (n.startsWith("find") || n.startsWith("get") || n.startsWith("query")
|
|
169
|
+
|| n.startsWith("read") || n.startsWith("count") || n.startsWith("exists")
|
|
170
|
+
|| n.startsWith("select") || n.startsWith("search")) {
|
|
171
|
+
return "READS_FROM";
|
|
172
|
+
}
|
|
173
|
+
if (n.startsWith("save") || n.startsWith("insert") || n.startsWith("update")
|
|
174
|
+
|| n.startsWith("delete") || n.startsWith("remove") || n.startsWith("persist")
|
|
175
|
+
|| n.startsWith("merge") || n.startsWith("flush")) {
|
|
176
|
+
return "WRITES_TO";
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private static String parentClassId(Map<String, Object> methodNode) {
|
|
182
|
+
if (!"METHOD".equals(methodNode.get("type"))) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
String id = (String) methodNode.get("id");
|
|
186
|
+
int lastDot = id.lastIndexOf('.');
|
|
187
|
+
int colon = id.lastIndexOf("::");
|
|
188
|
+
return lastDot > colon ? id.substring(0, lastDot) : id;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private static String ownerClassId(Map<String, Object> methodNode) {
|
|
192
|
+
String id = parentClassId(methodNode);
|
|
193
|
+
return id == null ? (String) methodNode.get("id") : id;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private static String packageOf(String relPath) {
|
|
197
|
+
String p = relPath;
|
|
198
|
+
for (String root : new String[]{"src/main/java/", "src/test/java/"}) {
|
|
199
|
+
if (p.startsWith(root)) {
|
|
200
|
+
p = p.substring(root.length());
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (p.endsWith(".java")) {
|
|
205
|
+
p = p.substring(0, p.length() - 5);
|
|
206
|
+
}
|
|
207
|
+
return p.replace('/', '.');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
package devlens.extractor.edges;
|
|
2
|
+
|
|
3
|
+
import devlens.extractor.Contract;
|
|
4
|
+
import devlens.extractor.LookupMaps;
|
|
5
|
+
import devlens.extractor.Parser;
|
|
6
|
+
|
|
7
|
+
import java.util.ArrayList;
|
|
8
|
+
import java.util.HashSet;
|
|
9
|
+
import java.util.LinkedHashMap;
|
|
10
|
+
import java.util.List;
|
|
11
|
+
import java.util.Map;
|
|
12
|
+
import java.util.Set;
|
|
13
|
+
import java.util.regex.Matcher;
|
|
14
|
+
import java.util.regex.Pattern;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Spring MVC route detection (mirrors edges/routes/*.py).
|
|
18
|
+
*
|
|
19
|
+
* Annotation-driven — no type resolution needed:
|
|
20
|
+
* class-level @RestController / @Controller (+ @RequestMapping base path)
|
|
21
|
+
* method-level @GetMapping/@PostMapping/@PutMapping/@DeleteMapping/
|
|
22
|
+
* @PatchMapping/@RequestMapping(method = RequestMethod.X)
|
|
23
|
+
* Class prefix + method path compose; {id} path params → isDynamic + params.
|
|
24
|
+
*
|
|
25
|
+
* V1 scope: Spring MVC only. JAX-RS (@Path), @FeignClient, WebFlux
|
|
26
|
+
* functional routes are V2 (documented in java_graph_detection.html).
|
|
27
|
+
*/
|
|
28
|
+
public final class Routes {
|
|
29
|
+
|
|
30
|
+
private static final Pattern PATH_PARAM = Pattern.compile("\\{([^}]+)\\}");
|
|
31
|
+
|
|
32
|
+
private static final List<String> MAPPING_ANNOTATIONS = List.of(
|
|
33
|
+
"RequestMapping", "GetMapping", "PostMapping", "PutMapping",
|
|
34
|
+
"DeleteMapping", "PatchMapping");
|
|
35
|
+
|
|
36
|
+
private Routes() {}
|
|
37
|
+
|
|
38
|
+
public static final class RouteResult {
|
|
39
|
+
public final List<Map<String, Object>> routes = new ArrayList<>();
|
|
40
|
+
public final List<Map<String, Object>> routeNodes = new ArrayList<>();
|
|
41
|
+
public final List<Map<String, Object>> handlesEdges = new ArrayList<>();
|
|
42
|
+
final Set<String> seen = new HashSet<>();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static RouteResult resolve(List<Parser.ParsedFile> files, LookupMaps lookup,
|
|
46
|
+
String framework) {
|
|
47
|
+
RouteResult result = new RouteResult();
|
|
48
|
+
String fw = framework == null || framework.equals("unknown") ? "spring" : framework;
|
|
49
|
+
for (Parser.ParsedFile pf : files) {
|
|
50
|
+
if (pf.isTest) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (Parser.TypeInfo t : pf.allTypes()) {
|
|
54
|
+
if (!isController(t)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
String base = pathValue(t.annotationValues.get("RequestMapping"));
|
|
58
|
+
for (Parser.MethodInfo m : t.methods) {
|
|
59
|
+
for (String ann : MAPPING_ANNOTATIONS) {
|
|
60
|
+
if (!m.annotations.contains(ann)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
String methodPath = pathValue(m.annotationValues.get(ann));
|
|
64
|
+
String verb = verbFor(ann, m.annotationValues.get(ann + ".method"));
|
|
65
|
+
String urlPath = join(base, methodPath);
|
|
66
|
+
emit(pf, t, m, verb, urlPath, fw, result);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static boolean isController(Parser.TypeInfo t) {
|
|
75
|
+
return t.annotations.contains("RestController") || t.annotations.contains("Controller");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private static String verbFor(String ann, String methodAttr) {
|
|
79
|
+
if (!ann.equals("RequestMapping")) {
|
|
80
|
+
return ann.replace("Mapping", "").toUpperCase();
|
|
81
|
+
}
|
|
82
|
+
return methodAttr == null ? "ANY" : methodAttr.toUpperCase();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static String pathValue(String raw) {
|
|
86
|
+
return raw == null ? "" : raw.trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static String join(String base, String methodPath) {
|
|
90
|
+
String b = base == null ? "" : base;
|
|
91
|
+
String m = methodPath == null ? "" : methodPath;
|
|
92
|
+
if (b.isEmpty()) {
|
|
93
|
+
return m.isEmpty() ? "/" : normalize(m);
|
|
94
|
+
}
|
|
95
|
+
if (m.isEmpty()) {
|
|
96
|
+
return normalize(b);
|
|
97
|
+
}
|
|
98
|
+
if (m.startsWith("/")) {
|
|
99
|
+
m = m.substring(1);
|
|
100
|
+
}
|
|
101
|
+
return normalize(b + "/" + m);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private static String normalize(String p) {
|
|
105
|
+
String out = p;
|
|
106
|
+
if (!out.startsWith("/")) {
|
|
107
|
+
out = "/" + out;
|
|
108
|
+
}
|
|
109
|
+
while (out.endsWith("/") && out.length() > 1) {
|
|
110
|
+
out = out.substring(0, out.length() - 1);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private static void emit(Parser.ParsedFile pf, Parser.TypeInfo t, Parser.MethodInfo m,
|
|
116
|
+
String verb, String urlPath, String fw, RouteResult result) {
|
|
117
|
+
String key = pf.relPath + "|" + verb + "|" + urlPath;
|
|
118
|
+
if (!result.seen.add(key)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
Matcher matcher = PATH_PARAM.matcher(urlPath);
|
|
122
|
+
boolean dynamic = matcher.find();
|
|
123
|
+
List<String> params = new ArrayList<>();
|
|
124
|
+
matcher.reset();
|
|
125
|
+
while (matcher.find()) {
|
|
126
|
+
params.add(matcher.group(1));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
String handlerId = pf.relPath + "::" + t.dottedName + "." + m.name;
|
|
130
|
+
Map<String, Object> routeNode = Contract.routeNode(pf.relPath, verb, urlPath,
|
|
131
|
+
dynamic, params, fw, m.name);
|
|
132
|
+
Map<String, Object> backendRoute = Contract.backendRoute(urlPath, pf.relPath,
|
|
133
|
+
verb, fw, dynamic, params, m.name, (String) routeNode.get("id"));
|
|
134
|
+
|
|
135
|
+
result.routeNodes.add(routeNode);
|
|
136
|
+
result.routes.add(backendRoute);
|
|
137
|
+
Map<String, Object> handlesMeta = new LinkedHashMap<>();
|
|
138
|
+
handlesMeta.put("urlPath", urlPath);
|
|
139
|
+
handlesMeta.put("httpMethod", verb);
|
|
140
|
+
handlesMeta.put("routeKind", "backend");
|
|
141
|
+
result.handlesEdges.add(Contract.edge("HANDLES", (String) routeNode.get("id"), handlerId, handlesMeta));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package devlens.extractor.edges;
|
|
2
|
+
|
|
3
|
+
import devlens.extractor.Contract;
|
|
4
|
+
import devlens.extractor.LookupMaps;
|
|
5
|
+
|
|
6
|
+
import java.util.ArrayList;
|
|
7
|
+
import java.util.List;
|
|
8
|
+
import java.util.Map;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* TESTS edge resolution (mirrors edges/tests.py + JS testEdges).
|
|
12
|
+
*
|
|
13
|
+
* Test files are LEAF nodes — the parser still extracted their test methods
|
|
14
|
+
* (names land in metadata.testCases), but no test children enter the graph.
|
|
15
|
+
* TESTS edges point from the TEST file node → the production CLASS nodes it
|
|
16
|
+
* imports (symbol maps are the bridge — test files import production types).
|
|
17
|
+
*/
|
|
18
|
+
public final class Tests {
|
|
19
|
+
|
|
20
|
+
private Tests() {}
|
|
21
|
+
|
|
22
|
+
public static void resolve(LookupMaps lookup, List<Map<String, Object>> edgesOut) {
|
|
23
|
+
for (Map.Entry<String, Map<String, Object>> entry : lookup.fileNodesByPath.entrySet()) {
|
|
24
|
+
Map<String, Object> fileNode = entry.getValue();
|
|
25
|
+
if (!"TEST".equals(fileNode.get("type"))) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
String relPath = entry.getKey();
|
|
29
|
+
// metadata.testCases = @Test methods extracted at parse time
|
|
30
|
+
List<String> cases = lookup.testCases.get(relPath);
|
|
31
|
+
if (cases != null && !cases.isEmpty()) {
|
|
32
|
+
((Map<String, Object>) fileNode.get("metadata")).put("testCases", cases);
|
|
33
|
+
}
|
|
34
|
+
// production symbols this test file imports
|
|
35
|
+
Map<String, String> sm = lookup.symbolMap(relPath);
|
|
36
|
+
List<String> targets = new ArrayList<>(sm.values());
|
|
37
|
+
java.util.Collections.sort(targets);
|
|
38
|
+
for (String target : targets) {
|
|
39
|
+
if (target.startsWith("[mvn]")) {
|
|
40
|
+
continue; // don't test third-party libs
|
|
41
|
+
}
|
|
42
|
+
Map<String, Object> node = lookup.nodeById.get(target);
|
|
43
|
+
if (node == null) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
String type = (String) node.get("type");
|
|
47
|
+
if ("CLASS".equals(type) || "INTERFACE".equals(type) || "ENUM".equals(type)) {
|
|
48
|
+
edgesOut.add(Contract.edge("TESTS", "file::" + relPath, target, null));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Entry point — invoked as: python3 -m devlens_extractors_python
|
|
2
|
+
|
|
3
|
+
Reads the ExtractorInput JSON from stdin, runs extraction, writes the
|
|
4
|
+
ExtractorResult JSON to stdout. stdout must stay PURE JSON (the Node.js
|
|
5
|
+
orchestrator JSON.parses it verbatim) — all logging goes to stderr.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from .extractor import extract
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
raw = sys.stdin.read()
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
payload = json.loads(raw)
|
|
19
|
+
except json.JSONDecodeError as exc:
|
|
20
|
+
print(f"devlens extractor: invalid JSON on stdin: {exc}", file=sys.stderr)
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
repo_path = payload.get("repoPath")
|
|
24
|
+
if not repo_path:
|
|
25
|
+
print("devlens extractor: missing 'repoPath' in input", file=sys.stderr)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
options = payload.get("options") or {}
|
|
29
|
+
result = extract(repo_path, options)
|
|
30
|
+
sys.stdout.write(json.dumps(result) + "\n")
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
contract Types - the JSON shape Devlens Engine expects.
|
|
3
|
+
|
|
4
|
+
Every key here is camel case to match the typescript interface in the devlens-engine. This file is the single
|
|
5
|
+
source of truth for the output shape; other modules never spell keys by hand.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
import hashlib
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
def code_hash(raw_code: str) -> str:
|
|
15
|
+
"""Return a SHA256 hash of the raw code string."""
|
|
16
|
+
return hashlib.sha256(raw_code.encode("utf-8")).hexdigest()[:16] # first 16 chars of the hash
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Fingerprint:
|
|
20
|
+
language: str = "python"
|
|
21
|
+
projectType: str = "unknown" # "backend" once a framework is detected
|
|
22
|
+
framework: str = "unknown" # "fastapi" | "flask" | "django" | "unknown"
|
|
23
|
+
router: str = "none"
|
|
24
|
+
stateManagement: list = field(default_factory=list) # always [] for Python
|
|
25
|
+
dataFetching: list = field(default_factory=list) # always [] for Python
|
|
26
|
+
databases: list = field(default_factory=list)
|
|
27
|
+
rawDependencies: dict = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict:
|
|
30
|
+
return {
|
|
31
|
+
"language": self.language,
|
|
32
|
+
"projectType": self.projectType,
|
|
33
|
+
"framework": self.framework,
|
|
34
|
+
"router": self.router,
|
|
35
|
+
"stateManagement": self.stateManagement,
|
|
36
|
+
"dataFetching": self.dataFetching,
|
|
37
|
+
"databases": self.databases,
|
|
38
|
+
"rawDependencies": self.rawDependencies,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Stats:
|
|
43
|
+
totalFiles: int = 0
|
|
44
|
+
totalNodes: int = 0
|
|
45
|
+
skippedFiles: int = 0
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict:
|
|
48
|
+
return {
|
|
49
|
+
"totalFiles": self.totalFiles,
|
|
50
|
+
"totalNodes": self.totalNodes,
|
|
51
|
+
"skippedFiles": self.skippedFiles,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ExtractorError:
|
|
56
|
+
file: str
|
|
57
|
+
error: str
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict:
|
|
60
|
+
return {"file": self.file, "error": self.error}
|
|
61
|
+
|
|
62
|
+
def file_node(rel_path: str, end_line: int, node_type: str = "FILE",
|
|
63
|
+
language: str = "python") -> dict:
|
|
64
|
+
"""FILE node — id format: file::rel/path.py (matches src/parser/index.ts)."""
|
|
65
|
+
return {
|
|
66
|
+
"id": f"file::{rel_path}",
|
|
67
|
+
"name": rel_path.split("/")[-1],
|
|
68
|
+
"type": node_type, # FILE | TEST
|
|
69
|
+
"filePath": rel_path,
|
|
70
|
+
"startLine": 1,
|
|
71
|
+
"endLine": end_line,
|
|
72
|
+
"parentFile": f"file::{rel_path}", # file nodes parent themselves
|
|
73
|
+
"metadata": {
|
|
74
|
+
"nodeCount": 0,
|
|
75
|
+
"childNodeIds": [],
|
|
76
|
+
"language": language,
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def code_node(rel_path: str, name: str, node_type: str, start_line: int, end_line: int,
|
|
81
|
+
raw_code: str, metadata: dict, language: str = "python") -> dict:
|
|
82
|
+
"""Function/Class/Method node. ID format: rel/path.py::name."""
|
|
83
|
+
return {
|
|
84
|
+
"id": f"{rel_path}::{name}",
|
|
85
|
+
"name": name,
|
|
86
|
+
"type": node_type,
|
|
87
|
+
"filePath": rel_path,
|
|
88
|
+
"startLine": start_line,
|
|
89
|
+
"endLine": end_line,
|
|
90
|
+
"rawCode": raw_code,
|
|
91
|
+
"codeHash": code_hash(raw_code),
|
|
92
|
+
"parentFile": f"file::{rel_path}",
|
|
93
|
+
"metadata": {"language": language, **metadata},
|
|
94
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Edge detectors — import resolution, call resolution, routes, ORM data layer.
|
|
2
|
+
|
|
3
|
+
Public API (all consume the shared LookupMaps built once by extractor.py):
|
|
4
|
+
resolve_file_imports(parsed, lookup, registry) -> (edges, imported_paths)
|
|
5
|
+
resolve_calls(lookup, registry) -> edges
|
|
6
|
+
detect_routes(parsed_files, lookup, fingerprint)-> RouteResult
|
|
7
|
+
detect_models(lookup) -> {model name → node id}
|
|
8
|
+
resolve_orm_edges(lookup, models) -> edges
|
|
9
|
+
"""
|
|
10
|
+
from .calls import resolve_calls
|
|
11
|
+
from .enrich import enrich_metadata
|
|
12
|
+
from .imports import resolve_file_imports
|
|
13
|
+
from .inheritance import resolve_inheritance
|
|
14
|
+
from .orm_edges import detect_models, resolve_orm_edges
|
|
15
|
+
from .routes import RouteResult, detect_routes
|
|
16
|
+
from .tests import resolve_test_edges
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"resolve_file_imports",
|
|
20
|
+
"resolve_calls",
|
|
21
|
+
"detect_routes",
|
|
22
|
+
"RouteResult",
|
|
23
|
+
"detect_models",
|
|
24
|
+
"resolve_orm_edges",
|
|
25
|
+
"enrich_metadata",
|
|
26
|
+
"resolve_inheritance",
|
|
27
|
+
"resolve_test_edges",
|
|
28
|
+
]
|