Skip to content

Commit e3e831a

Browse files
aksOpsclaude
andcommitted
feat: detectors emit infrastructure edges using InfrastructureRegistry
10 detectors updated to emit CONNECTS_TO/CALLS/PRODUCES/CONSUMES edges to infrastructure endpoints resolved via ctx.registry(). Falls back to database:unknown or external:unknown when no registry match. Detectors updated: JpaEntity, Repository, Kafka, SpringRest (Java); DjangoModel, SQLAlchemyModel (Python); TypeORM, PrismaORM, KafkaJS, NestJSController (TypeScript). All 1298 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3627019 commit e3e831a

14 files changed

Lines changed: 458 additions & 50 deletions

src/main/java/io/github/randomcodespace/iq/detector/java/JpaEntityDetector.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
description = "Detects JPA/Hibernate entities (@Entity, @Table, column mappings)",
3232
parser = ParserType.JAVAPARSER,
3333
languages = {"java"},
34-
nodeKinds = {NodeKind.ENTITY},
35-
edgeKinds = {EdgeKind.MAPS_TO},
34+
nodeKinds = {NodeKind.ENTITY, NodeKind.DATABASE_CONNECTION},
35+
edgeKinds = {EdgeKind.MAPS_TO, EdgeKind.CONNECTS_TO},
3636
properties = {"columns", "table_name"}
3737
)
3838
@Component
@@ -144,6 +144,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) {
144144
node.setAnnotations(new ArrayList<>(List.of("@Entity")));
145145
node.setProperties(properties);
146146
nodes.add(node);
147+
addDbEdge(entityId, ctx.registry(), nodes, edges);
147148

148149
// Extract relationship edges from fields
149150
for (FieldDeclaration field : classDecl.getFields()) {
@@ -275,6 +276,7 @@ private DetectorResult detectWithRegex(DetectorContext ctx) {
275276
node.setAnnotations(new ArrayList<>(List.of("@Entity")));
276277
node.setProperties(properties);
277278
nodes.add(node);
279+
addDbEdge(entityId, ctx.registry(), nodes, edges);
278280

279281
for (int i = 0; i < lines.length; i++) {
280282
Matcher relMatch = RELATIONSHIP_REGEX.matcher(lines[i]);
@@ -320,4 +322,46 @@ private DetectorResult detectWithRegex(DetectorContext ctx) {
320322

321323
return DetectorResult.of(nodes, edges);
322324
}
325+
326+
// ==================== InfrastructureRegistry helpers ====================
327+
328+
private static String ensureDbNode(
329+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry,
330+
List<CodeNode> nodes) {
331+
String dbNodeId;
332+
if (registry != null && !registry.getDatabases().isEmpty()) {
333+
io.github.randomcodespace.iq.analyzer.InfraEndpoint db =
334+
registry.getDatabases().values().iterator().next();
335+
dbNodeId = "infra:" + db.id();
336+
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
337+
CodeNode dbNode = new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION,
338+
db.name() + " (" + db.type() + ")");
339+
dbNode.getProperties().put("type", db.type());
340+
if (db.connectionUrl() != null) dbNode.getProperties().put("url", db.connectionUrl());
341+
nodes.add(dbNode);
342+
}
343+
} else {
344+
dbNodeId = "database:unknown";
345+
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
346+
nodes.add(new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
347+
}
348+
}
349+
return dbNodeId;
350+
}
351+
352+
private static void addDbEdge(String sourceId,
353+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry,
354+
List<CodeNode> nodes, List<CodeEdge> edges) {
355+
String dbNodeId = ensureDbNode(registry, nodes);
356+
CodeNode targetRef = nodes.stream()
357+
.filter(n -> dbNodeId.equals(n.getId()))
358+
.findFirst()
359+
.orElseGet(() -> new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
360+
CodeEdge edge = new CodeEdge();
361+
edge.setId(sourceId + "->connects_to->" + dbNodeId);
362+
edge.setKind(EdgeKind.CONNECTS_TO);
363+
edge.setSourceId(sourceId);
364+
edge.setTarget(targetRef);
365+
edges.add(edge);
366+
}
323367
}

src/main/java/io/github/randomcodespace/iq/detector/java/KafkaDetector.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public DetectorResult detect(DetectorContext ctx) {
7373

7474
String classNodeId = ctx.filePath() + ":" + className;
7575
Set<String> seenTopics = new LinkedHashSet<>();
76+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry = ctx.registry();
7677

7778
// @KafkaListener consumers
7879
for (int i = 0; i < lines.length; i++) {
@@ -82,7 +83,7 @@ public DetectorResult detect(DetectorContext ctx) {
8283
Matcher fallback = Pattern.compile("\"([^\"]+)\"").matcher(lines[i]);
8384
if (fallback.find()) {
8485
String topic = fallback.group(1);
85-
String topicId = ensureTopicNode(topic, seenTopics, nodes);
86+
String topicId = ensureTopicNode(topic, seenTopics, nodes, registry);
8687
Map<String, Object> props = new LinkedHashMap<>();
8788
props.put("topic", topic);
8889
addEdge(classNodeId, topicId, EdgeKind.CONSUMES,
@@ -92,7 +93,7 @@ public DetectorResult detect(DetectorContext ctx) {
9293
continue;
9394
}
9495
String topic = m.group(1);
95-
String topicId = ensureTopicNode(topic, seenTopics, nodes);
96+
String topicId = ensureTopicNode(topic, seenTopics, nodes, registry);
9697
Map<String, Object> props = new LinkedHashMap<>();
9798
props.put("topic", topic);
9899
Matcher gm = GROUP_ID_RE.matcher(lines[i]);
@@ -106,7 +107,7 @@ public DetectorResult detect(DetectorContext ctx) {
106107
Matcher m = KAFKA_SEND_RE.matcher(lines[i]);
107108
if (!m.find()) continue;
108109
String topic = m.group(1);
109-
String topicId = ensureTopicNode(topic, seenTopics, nodes);
110+
String topicId = ensureTopicNode(topic, seenTopics, nodes, registry);
110111
addEdge(classNodeId, topicId, EdgeKind.PRODUCES,
111112
className + " produces to " + topic,
112113
Map.of("topic", topic), edges, nodes);
@@ -115,8 +116,14 @@ public DetectorResult detect(DetectorContext ctx) {
115116
return DetectorResult.of(nodes, edges);
116117
}
117118

118-
private String ensureTopicNode(String topic, Set<String> seen, List<CodeNode> nodes) {
119+
private String ensureTopicNode(String topic, Set<String> seen, List<CodeNode> nodes,
120+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry) {
121+
// Use canonical registry id if this topic is registered
119122
String topicId = "kafka:topic:" + topic;
123+
if (registry != null) {
124+
io.github.randomcodespace.iq.analyzer.InfraEndpoint registered = registry.getTopics().get(topic);
125+
if (registered != null) topicId = "infra:" + registered.id();
126+
}
120127
if (!seen.contains(topic)) {
121128
seen.add(topic);
122129
CodeNode node = new CodeNode();

src/main/java/io/github/randomcodespace/iq/detector/java/RepositoryDetector.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
category = "entities",
2323
description = "Detects Spring Data repositories and custom query methods",
2424
languages = {"java"},
25-
nodeKinds = {NodeKind.ENTITY, NodeKind.REPOSITORY},
26-
edgeKinds = {EdgeKind.QUERIES},
25+
nodeKinds = {NodeKind.ENTITY, NodeKind.REPOSITORY, NodeKind.DATABASE_CONNECTION},
26+
edgeKinds = {EdgeKind.QUERIES, EdgeKind.CONNECTS_TO},
2727
properties = {"custom_queries", "method"}
2828
)
2929
@Component
@@ -155,7 +155,50 @@ public DetectorResult detect(DetectorContext ctx) {
155155
edge.setTarget(targetRef);
156156
edges.add(edge);
157157
}
158+
addDbEdge(repoId, ctx.registry(), nodes, edges);
158159

159160
return DetectorResult.of(nodes, edges);
160161
}
162+
163+
// ==================== InfrastructureRegistry helpers ====================
164+
165+
private static String ensureDbNode(
166+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry,
167+
List<CodeNode> nodes) {
168+
String dbNodeId;
169+
if (registry != null && !registry.getDatabases().isEmpty()) {
170+
io.github.randomcodespace.iq.analyzer.InfraEndpoint db =
171+
registry.getDatabases().values().iterator().next();
172+
dbNodeId = "infra:" + db.id();
173+
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
174+
CodeNode dbNode = new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION,
175+
db.name() + " (" + db.type() + ")");
176+
dbNode.getProperties().put("type", db.type());
177+
if (db.connectionUrl() != null) dbNode.getProperties().put("url", db.connectionUrl());
178+
nodes.add(dbNode);
179+
}
180+
} else {
181+
dbNodeId = "database:unknown";
182+
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
183+
nodes.add(new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
184+
}
185+
}
186+
return dbNodeId;
187+
}
188+
189+
private static void addDbEdge(String sourceId,
190+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry,
191+
List<CodeNode> nodes, List<CodeEdge> edges) {
192+
String dbNodeId = ensureDbNode(registry, nodes);
193+
CodeNode targetRef = nodes.stream()
194+
.filter(n -> dbNodeId.equals(n.getId()))
195+
.findFirst()
196+
.orElseGet(() -> new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
197+
CodeEdge edge = new CodeEdge();
198+
edge.setId(sourceId + "->connects_to->" + dbNodeId);
199+
edge.setKind(EdgeKind.CONNECTS_TO);
200+
edge.setSourceId(sourceId);
201+
edge.setTarget(targetRef);
202+
edges.add(edge);
203+
}
161204
}

src/main/java/io/github/randomcodespace/iq/detector/java/SpringRestDetector.java

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
parser = ParserType.JAVAPARSER,
3030
languages = {"java"},
3131
nodeKinds = {NodeKind.ENDPOINT},
32-
edgeKinds = {EdgeKind.EXPOSES},
32+
edgeKinds = {EdgeKind.EXPOSES, EdgeKind.CALLS},
3333
properties = {"consumes", "http_method", "method", "path", "produces"}
3434
)
3535
@Component
@@ -55,6 +55,12 @@ public class SpringRestDetector extends AbstractJavaParserDetector {
5555
"PatchMapping", "PATCH"
5656
);
5757

58+
// ---- HTTP client patterns (for CALLS edge emission) ----
59+
private static final Pattern REST_TEMPLATE_RE = Pattern.compile("RestTemplate");
60+
private static final Pattern WEB_CLIENT_RE = Pattern.compile("WebClient");
61+
private static final Pattern FEIGN_CLIENT_RE = Pattern.compile(
62+
"@FeignClient\\s*\\(\\s*(?:name\\s*=\\s*)?[\"']([^\"']+)[\"']");
63+
5864
@Override
5965
public String getName() {
6066
return "spring_rest";
@@ -174,6 +180,9 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) {
174180
}
175181
});
176182

183+
// HTTP client calls → CALLS edge
184+
addHttpClientEdges(ctx, nodes, edges);
185+
177186
return DetectorResult.of(nodes, edges);
178187
}
179188

@@ -346,6 +355,9 @@ private DetectorResult detectWithRegex(DetectorContext ctx) {
346355
edges.add(edge);
347356
}
348357

358+
// HTTP client calls → CALLS edge
359+
addHttpClientEdges(ctx, nodes, edges);
360+
349361
return DetectorResult.of(nodes, edges);
350362
}
351363

@@ -354,4 +366,66 @@ private static String extractAttr(String attrStr, Pattern pattern) {
354366
Matcher m = pattern.matcher(attrStr);
355367
return m.find() ? m.group(1) : null;
356368
}
369+
370+
// ==================== HTTP client / InfrastructureRegistry helpers ====================
371+
372+
private static void addHttpClientEdges(DetectorContext ctx,
373+
List<CodeNode> nodes, List<CodeEdge> edges) {
374+
String text = ctx.content();
375+
boolean hasRestTemplate = REST_TEMPLATE_RE.matcher(text).find();
376+
boolean hasWebClient = WEB_CLIENT_RE.matcher(text).find();
377+
Matcher feignMatcher = FEIGN_CLIENT_RE.matcher(text);
378+
boolean hasFeignClient = feignMatcher.find();
379+
if (!hasRestTemplate && !hasWebClient && !hasFeignClient) return;
380+
381+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry = ctx.registry();
382+
String clientType = hasRestTemplate ? "RestTemplate"
383+
: hasWebClient ? "WebClient"
384+
: "FeignClient";
385+
386+
// Try to match FeignClient name against registry external APIs
387+
String targetId;
388+
String targetLabel;
389+
if (hasFeignClient && registry != null) {
390+
String feignName = feignMatcher.group(1);
391+
io.github.randomcodespace.iq.analyzer.InfraEndpoint matched =
392+
registry.getExternalApis().values().stream()
393+
.filter(e -> feignName.equalsIgnoreCase(e.name()))
394+
.findFirst().orElse(null);
395+
if (matched != null) {
396+
targetId = "infra:" + matched.id();
397+
targetLabel = matched.name();
398+
} else {
399+
targetId = "external:" + feignName;
400+
targetLabel = feignName;
401+
}
402+
} else if (registry != null && !registry.getExternalApis().isEmpty()) {
403+
io.github.randomcodespace.iq.analyzer.InfraEndpoint api =
404+
registry.getExternalApis().values().iterator().next();
405+
targetId = "infra:" + api.id();
406+
targetLabel = api.name();
407+
} else {
408+
targetId = "external:unknown";
409+
targetLabel = "External API";
410+
}
411+
412+
if (nodes.stream().noneMatch(n -> targetId.equals(n.getId()))) {
413+
CodeNode apiNode = new CodeNode(targetId, NodeKind.ENDPOINT, targetLabel);
414+
apiNode.getProperties().put("type", "external_api");
415+
nodes.add(apiNode);
416+
}
417+
418+
String sourceId = ctx.filePath();
419+
CodeNode targetRef = nodes.stream()
420+
.filter(n -> targetId.equals(n.getId()))
421+
.findFirst()
422+
.orElseGet(() -> new CodeNode(targetId, NodeKind.ENDPOINT, targetLabel));
423+
CodeEdge edge = new CodeEdge();
424+
edge.setId(sourceId + "->calls->" + targetId);
425+
edge.setKind(EdgeKind.CALLS);
426+
edge.setSourceId(sourceId);
427+
edge.setTarget(targetRef);
428+
edge.getProperties().put("client_type", clientType);
429+
edges.add(edge);
430+
}
357431
}

src/main/java/io/github/randomcodespace/iq/detector/python/DjangoModelDetector.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
description = "Detects Django ORM models and managers",
3232
parser = ParserType.ANTLR,
3333
languages = {"python"},
34-
nodeKinds = {NodeKind.ENTITY, NodeKind.REPOSITORY},
35-
edgeKinds = {EdgeKind.DEPENDS_ON, EdgeKind.QUERIES},
34+
nodeKinds = {NodeKind.ENTITY, NodeKind.REPOSITORY, NodeKind.DATABASE_CONNECTION},
35+
edgeKinds = {EdgeKind.DEPENDS_ON, EdgeKind.QUERIES, EdgeKind.CONNECTS_TO},
3636
properties = {"framework", "table_name"}
3737
)
3838
@Component
@@ -185,6 +185,7 @@ public void enterClassdef(Python3Parser.ClassdefContext classCtx) {
185185
node.getProperties().put("ordering", ordering);
186186
}
187187
nodes.add(node);
188+
DjangoModelDetector.addDbEdge(nodeId, ctx.registry(), nodes, edges);
188189

189190
// FK / OneToOne edges
190191
Matcher fkMatcher = FK_RE.matcher(classBody);
@@ -323,6 +324,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) {
323324
node.getProperties().put("ordering", ordering);
324325
}
325326
nodes.add(node);
327+
addDbEdge(nodeId, ctx.registry(), nodes, edges);
326328

327329
Matcher fkMatcher = FK_RE.matcher(classBody);
328330
while (fkMatcher.find()) {
@@ -382,4 +384,46 @@ private static String extractClassBody(String text, Python3Parser.ClassdefContex
382384
int stop = classCtx.getStop() != null ? classCtx.getStop().getStopIndex() + 1 : text.length();
383385
return text.substring(Math.min(start, text.length()), Math.min(stop, text.length()));
384386
}
387+
388+
// ==================== InfrastructureRegistry helpers ====================
389+
390+
static String ensureDbNode(
391+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry,
392+
List<CodeNode> nodes) {
393+
String dbNodeId;
394+
if (registry != null && !registry.getDatabases().isEmpty()) {
395+
io.github.randomcodespace.iq.analyzer.InfraEndpoint db =
396+
registry.getDatabases().values().iterator().next();
397+
dbNodeId = "infra:" + db.id();
398+
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
399+
CodeNode dbNode = new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION,
400+
db.name() + " (" + db.type() + ")");
401+
dbNode.getProperties().put("type", db.type());
402+
if (db.connectionUrl() != null) dbNode.getProperties().put("url", db.connectionUrl());
403+
nodes.add(dbNode);
404+
}
405+
} else {
406+
dbNodeId = "database:unknown";
407+
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
408+
nodes.add(new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
409+
}
410+
}
411+
return dbNodeId;
412+
}
413+
414+
static void addDbEdge(String sourceId,
415+
io.github.randomcodespace.iq.analyzer.InfrastructureRegistry registry,
416+
List<CodeNode> nodes, List<CodeEdge> edges) {
417+
String dbNodeId = ensureDbNode(registry, nodes);
418+
CodeNode targetRef = nodes.stream()
419+
.filter(n -> dbNodeId.equals(n.getId()))
420+
.findFirst()
421+
.orElseGet(() -> new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
422+
CodeEdge edge = new CodeEdge();
423+
edge.setId(sourceId + "->connects_to->" + dbNodeId);
424+
edge.setKind(EdgeKind.CONNECTS_TO);
425+
edge.setSourceId(sourceId);
426+
edge.setTarget(targetRef);
427+
edges.add(edge);
428+
}
385429
}

0 commit comments

Comments
 (0)