Skip to content

Commit 7cec5fd

Browse files
fix(detection): fix TopicLinker SENDS_TO gap, ConfigDefDetector, KafkaDetector Kotlin, and ReactComponentDetector RENDERS scoping
- TopicLinker: handle SENDS_TO/RECEIVES_FROM edges from TIBCO EMS, IBM MQ, and Azure Messaging detectors so enterprise messaging patterns get cross-linked - ConfigDefDetector: implement actual @value and @ConfigurationProperties detection (was advertised in description but unimplemented); also detect Kafka ConfigDef.define() as before; fix description to match reality - KafkaDetector: add "kotlin" to supported languages so Kotlin/Spring Kafka codebases get @KafkaListener and KafkaTemplate coverage - ReactComponentDetector: scope RENDERS edge detection to each component's body section (from its match position to the next component's position) to prevent false RENDERS edges from multi-component files All fixes include new tests. Targeted Java tests pass: TopicLinkerTest (6), ReactComponentDetectorTest (5), JavaDetectorsTest$ConfigDefTests (6), JavaDetectorsTest$KafkaTests (5). Note: frontend build fails on CodeGraphView.tsx TypeScript errors that are pre-existing in the working tree (not caused by this PR). Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 parent f3de21b commit 7cec5fd

7 files changed

Lines changed: 374 additions & 80 deletions

File tree

src/main/java/io/github/randomcodespace/iq/analyzer/linker/TopicLinker.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
import java.util.TreeSet;
1919

2020
/**
21-
* Links Kafka/RabbitMQ producers to consumers via shared topic names.
21+
* Links messaging producers to consumers via shared topic/queue names.
2222
* <p>
23-
* Scans for TOPIC/QUEUE nodes and matches PRODUCES edges with CONSUMES
24-
* edges on the same topic label to create direct producer-to-consumer
25-
* CALLS edges.
23+
* Scans for TOPIC/QUEUE nodes and matches PRODUCES/SENDS_TO edges with
24+
* CONSUMES/RECEIVES_FROM edges on the same topic label to create direct
25+
* producer-to-consumer CALLS edges. Supports Kafka, RabbitMQ, TIBCO EMS,
26+
* IBM MQ, Azure Service Bus, and other enterprise messaging patterns.
2627
*/
2728
@Component
2829
public class TopicLinker implements Linker {
@@ -51,11 +52,13 @@ public LinkResult link(List<CodeNode> nodes, List<CodeEdge> edges) {
5152
Map<String, List<String>> consumersByTopic = new TreeMap<>();
5253

5354
for (CodeEdge edge : edges) {
54-
if (edge.getKind() == EdgeKind.PRODUCES && edge.getTarget() != null) {
55+
boolean isProducer = edge.getKind() == EdgeKind.PRODUCES || edge.getKind() == EdgeKind.SENDS_TO;
56+
boolean isConsumer = edge.getKind() == EdgeKind.CONSUMES || edge.getKind() == EdgeKind.RECEIVES_FROM;
57+
if (isProducer && edge.getTarget() != null) {
5558
producersByTopic
5659
.computeIfAbsent(edge.getTarget().getId(), k -> new ArrayList<>())
5760
.add(edge.getSourceId());
58-
} else if (edge.getKind() == EdgeKind.CONSUMES && edge.getTarget() != null) {
61+
} else if (isConsumer && edge.getTarget() != null) {
5962
consumersByTopic
6063
.computeIfAbsent(edge.getTarget().getId(), k -> new ArrayList<>())
6164
.add(edge.getSourceId());

src/main/java/io/github/randomcodespace/iq/detector/frontend/ReactComponentDetector.java

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ public DetectorResult detect(DetectorContext ctx) {
5555
List<CodeNode> nodes = new ArrayList<>();
5656
List<CodeEdge> edges = new ArrayList<>();
5757
String filePath = ctx.filePath();
58-
List<String> componentNames = new ArrayList<>();
58+
59+
// Track component name -> (sourceId, matchStart) for scoped JSX search
60+
record ComponentEntry(String name, String sourceId, int matchStart) {}
61+
List<ComponentEntry> componentEntries = new ArrayList<>();
62+
Set<String> componentNames = new LinkedHashSet<>();
5963

6064
// Function components
6165
for (Pattern pattern : List.of(EXPORT_DEFAULT_FUNC, EXPORT_CONST_ARROW, EXPORT_CONST_FC)) {
@@ -64,8 +68,9 @@ public DetectorResult detect(DetectorContext ctx) {
6468
String name = m.group(1);
6569
if (componentNames.contains(name)) continue;
6670
int line = text.substring(0, m.start()).split("\n", -1).length;
71+
String sourceId = "react:" + filePath + ":component:" + name;
6772
CodeNode node = new CodeNode();
68-
node.setId("react:" + filePath + ":component:" + name);
73+
node.setId(sourceId);
6974
node.setKind(NodeKind.COMPONENT);
7075
node.setLabel(name);
7176
node.setFqn(filePath + "::" + name);
@@ -75,6 +80,7 @@ public DetectorResult detect(DetectorContext ctx) {
7580
node.getProperties().put("component_type", "function");
7681
nodes.add(node);
7782
componentNames.add(name);
83+
componentEntries.add(new ComponentEntry(name, sourceId, m.start()));
7884
}
7985
}
8086

@@ -85,8 +91,9 @@ public DetectorResult detect(DetectorContext ctx) {
8591
String name = m.group(1);
8692
if (componentNames.contains(name)) continue;
8793
int line = text.substring(0, m.start()).split("\n", -1).length;
94+
String sourceId = "react:" + filePath + ":component:" + name;
8895
CodeNode node = new CodeNode();
89-
node.setId("react:" + filePath + ":component:" + name);
96+
node.setId(sourceId);
9097
node.setKind(NodeKind.COMPONENT);
9198
node.setLabel(name);
9299
node.setFqn(filePath + "::" + name);
@@ -96,6 +103,7 @@ public DetectorResult detect(DetectorContext ctx) {
96103
node.getProperties().put("component_type", "class");
97104
nodes.add(node);
98105
componentNames.add(name);
106+
componentEntries.add(new ComponentEntry(name, sourceId, m.start()));
99107
}
100108
}
101109

@@ -120,25 +128,35 @@ public DetectorResult detect(DetectorContext ctx) {
120128
}
121129
}
122130

123-
// RENDERS edges (JSX child components)
131+
// RENDERS edges: scope JSX tag search to each component's body section.
132+
// A component's body is from its match position to the next component's position.
124133
Set<String> allDetected = new HashSet<>(componentNames);
125134
allDetected.addAll(hookNames);
126-
Set<String> childNames = new TreeSet<>();
127-
Matcher jsxM = JSX_TAG.matcher(text);
128-
while (jsxM.find()) {
129-
String tag = jsxM.group(1);
130-
if (!allDetected.contains(tag)) {
131-
childNames.add(tag);
135+
136+
componentEntries.sort(Comparator.comparingInt(ComponentEntry::matchStart));
137+
138+
for (int i = 0; i < componentEntries.size(); i++) {
139+
ComponentEntry comp = componentEntries.get(i);
140+
int bodyStart = comp.matchStart();
141+
int bodyEnd = (i + 1 < componentEntries.size())
142+
? componentEntries.get(i + 1).matchStart()
143+
: text.length();
144+
String bodyText = text.substring(bodyStart, bodyEnd);
145+
146+
Set<String> childNames = new TreeSet<>();
147+
Matcher jsxM = JSX_TAG.matcher(bodyText);
148+
while (jsxM.find()) {
149+
String tag = jsxM.group(1);
150+
if (!allDetected.contains(tag)) {
151+
childNames.add(tag);
152+
}
132153
}
133-
}
134154

135-
for (String comp : componentNames) {
136-
String sourceId = "react:" + filePath + ":component:" + comp;
137155
for (String child : childNames) {
138156
CodeEdge edge = new CodeEdge();
139-
edge.setId(sourceId + ":renders:" + child);
157+
edge.setId(comp.sourceId() + ":renders:" + child);
140158
edge.setKind(EdgeKind.RENDERS);
141-
edge.setSourceId(sourceId);
159+
edge.setSourceId(comp.sourceId());
142160
edge.setTarget(new CodeNode(child, NodeKind.COMPONENT, child));
143161
edges.add(edge);
144162
}

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

Lines changed: 123 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
55
import com.github.javaparser.ast.body.FieldDeclaration;
66
import com.github.javaparser.ast.body.MethodDeclaration;
7+
import com.github.javaparser.ast.body.Parameter;
78
import com.github.javaparser.ast.expr.AnnotationExpr;
89
import com.github.javaparser.ast.expr.MethodCallExpr;
910
import com.github.javaparser.ast.expr.StringLiteralExpr;
@@ -22,13 +23,13 @@
2223
import io.github.randomcodespace.iq.detector.ParserType;
2324

2425
/**
25-
* Detects Kafka ConfigDef.define() configuration definitions and Spring @ConfigurationProperties
26-
* using JavaParser AST with regex fallback.
26+
* Detects Kafka ConfigDef.define() configuration definitions, Spring @Value bindings,
27+
* and Spring @ConfigurationProperties class-level prefix declarations.
2728
*/
2829
@DetectorInfo(
2930
name = "config_def",
3031
category = "config",
31-
description = "Detects Spring @Value and @ConfigurationProperties definitions",
32+
description = "Detects Kafka ConfigDef definitions, Spring @Value bindings, and @ConfigurationProperties prefixes",
3233
parser = ParserType.JAVAPARSER,
3334
languages = {"java"},
3435
nodeKinds = {NodeKind.CONFIG_DEFINITION},
@@ -40,6 +41,11 @@ public class ConfigDefDetector extends AbstractJavaParserDetector {
4041
// ---- Regex fallback patterns ----
4142
private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)");
4243
private static final Pattern DEFINE_RE = Pattern.compile("\\.define\\s*\\(\\s*\"([^\"]+)\"");
44+
private static final Pattern VALUE_RE = Pattern.compile("@Value\\s*\\(\\s*\"\\$\\{([^}]+)\\}\"");
45+
private static final Pattern CONFIG_PROPS_RE = Pattern.compile("@ConfigurationProperties\\s*\\(\\s*(?:prefix\\s*=\\s*)?\"([^\"]+)\"");
46+
47+
private static final String VALUE_ANNOTATION = "Value";
48+
private static final String CONFIG_PROPS_ANNOTATION = "ConfigurationProperties";
4349

4450
@Override
4551
public String getName() {
@@ -54,7 +60,13 @@ public Set<String> getSupportedLanguages() {
5460
@Override
5561
public DetectorResult detect(DetectorContext ctx) {
5662
String text = ctx.content();
57-
if (text == null || !text.contains("ConfigDef")) return DetectorResult.empty();
63+
if (text == null) return DetectorResult.empty();
64+
65+
boolean hasConfigDef = text.contains("ConfigDef");
66+
boolean hasValue = text.contains("@Value");
67+
boolean hasConfigProps = text.contains("@ConfigurationProperties");
68+
69+
if (!hasConfigDef && !hasValue && !hasConfigProps) return DetectorResult.empty();
5870

5971
Optional<CompilationUnit> cu = parse(ctx);
6072
if (cu.isPresent()) {
@@ -72,46 +84,102 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) {
7284
cu.findAll(ClassOrInterfaceDeclaration.class).forEach(classDecl -> {
7385
String className = classDecl.getNameAsString();
7486
String classNodeId = ctx.filePath() + ":" + className;
75-
7687
Set<String> seenKeys = new LinkedHashSet<>();
7788

78-
// Find all .define() method calls in the class
89+
// 1. Kafka ConfigDef.define() calls
7990
classDecl.findAll(MethodCallExpr.class).forEach(call -> {
8091
if (!"define".equals(call.getNameAsString())) return;
8192
if (call.getArguments().isEmpty()) return;
82-
8393
var firstArg = call.getArguments().get(0);
8494
if (!firstArg.isStringLiteralExpr()) return;
85-
8695
String configKey = firstArg.asStringLiteralExpr().getValue();
87-
if (seenKeys.contains(configKey)) return;
88-
seenKeys.add(configKey);
89-
90-
int line = call.getBegin().map(p -> p.line).orElse(1);
91-
92-
String nodeId = "config:" + configKey;
93-
CodeNode node = new CodeNode();
94-
node.setId(nodeId);
95-
node.setKind(NodeKind.CONFIG_DEFINITION);
96-
node.setLabel(configKey);
97-
node.setFilePath(ctx.filePath());
98-
node.setLineStart(line);
99-
node.getProperties().put("config_key", configKey);
100-
nodes.add(node);
101-
102-
CodeEdge edge = new CodeEdge();
103-
edge.setId(classNodeId + "->reads_config->" + nodeId);
104-
edge.setKind(EdgeKind.READS_CONFIG);
105-
edge.setSourceId(classNodeId);
106-
edge.setTarget(node);
107-
edge.setProperties(Map.of("config_key", configKey));
108-
edges.add(edge);
96+
if (seenKeys.add(configKey)) {
97+
int line = call.getBegin().map(p -> p.line).orElse(1);
98+
addConfigNode(configKey, "kafka_configdef", classNodeId, ctx.filePath(), line, nodes, edges);
99+
}
100+
});
101+
102+
// 2. Spring @Value("${some.key}") on fields and method parameters
103+
classDecl.findAll(FieldDeclaration.class).forEach(field -> {
104+
field.getAnnotations().forEach(ann -> {
105+
if (!VALUE_ANNOTATION.equals(ann.getNameAsString())) return;
106+
extractValueKey(ann).ifPresent(key -> {
107+
if (seenKeys.add(key)) {
108+
int line = ann.getBegin().map(p -> p.line).orElse(1);
109+
addConfigNode(key, "spring_value", classNodeId, ctx.filePath(), line, nodes, edges);
110+
}
111+
});
112+
});
113+
});
114+
115+
classDecl.findAll(MethodDeclaration.class).forEach(method -> {
116+
method.getParameters().forEach(param -> {
117+
param.getAnnotations().forEach(ann -> {
118+
if (!VALUE_ANNOTATION.equals(ann.getNameAsString())) return;
119+
extractValueKey(ann).ifPresent(key -> {
120+
if (seenKeys.add(key)) {
121+
int line = ann.getBegin().map(p -> p.line).orElse(1);
122+
addConfigNode(key, "spring_value", classNodeId, ctx.filePath(), line, nodes, edges);
123+
}
124+
});
125+
});
126+
});
127+
});
128+
129+
// 3. @ConfigurationProperties(prefix="some.prefix") on class
130+
classDecl.getAnnotations().forEach(ann -> {
131+
if (!CONFIG_PROPS_ANNOTATION.equals(ann.getNameAsString())) return;
132+
extractAnnotationStringValue(ann).ifPresent(prefix -> {
133+
if (seenKeys.add(prefix)) {
134+
int line = ann.getBegin().map(p -> p.line).orElse(1);
135+
addConfigNode(prefix, "spring_config_props", classNodeId, ctx.filePath(), line, nodes, edges);
136+
}
137+
});
109138
});
110139
});
111140

112141
return DetectorResult.of(nodes, edges);
113142
}
114143

144+
private Optional<String> extractValueKey(AnnotationExpr ann) {
145+
// @Value("${some.key}") or @Value(value = "${some.key}")
146+
String raw = ann.toString();
147+
Matcher m = Pattern.compile("\"\\$\\{([^}]+)\\}\"").matcher(raw);
148+
if (m.find()) return Optional.of(m.group(1));
149+
return Optional.empty();
150+
}
151+
152+
private Optional<String> extractAnnotationStringValue(AnnotationExpr ann) {
153+
// @ConfigurationProperties("prefix") or @ConfigurationProperties(prefix = "prefix")
154+
String raw = ann.toString();
155+
Matcher m = Pattern.compile("\"([^\"]+)\"").matcher(raw);
156+
if (m.find()) return Optional.of(m.group(1));
157+
return Optional.empty();
158+
}
159+
160+
private void addConfigNode(String key, String source, String classNodeId,
161+
String filePath, int line,
162+
List<CodeNode> nodes, List<CodeEdge> edges) {
163+
String nodeId = "config:" + key;
164+
CodeNode node = new CodeNode();
165+
node.setId(nodeId);
166+
node.setKind(NodeKind.CONFIG_DEFINITION);
167+
node.setLabel(key);
168+
node.setFilePath(filePath);
169+
node.setLineStart(line);
170+
node.getProperties().put("config_key", key);
171+
node.getProperties().put("config_source", source);
172+
nodes.add(node);
173+
174+
CodeEdge edge = new CodeEdge();
175+
edge.setId(classNodeId + "->reads_config->" + nodeId);
176+
edge.setKind(EdgeKind.READS_CONFIG);
177+
edge.setSourceId(classNodeId);
178+
edge.setTarget(node);
179+
edge.setProperties(Map.of("config_key", key));
180+
edges.add(edge);
181+
}
182+
115183
// ==================== Regex fallback ====================
116184

117185
private DetectorResult detectWithRegex(DetectorContext ctx) {
@@ -131,29 +199,32 @@ private DetectorResult detectWithRegex(DetectorContext ctx) {
131199
Set<String> seenKeys = new LinkedHashSet<>();
132200

133201
for (int i = 0; i < lines.length; i++) {
202+
// Kafka ConfigDef.define()
134203
Matcher m = DEFINE_RE.matcher(lines[i]);
135-
if (!m.find()) continue;
136-
String configKey = m.group(1);
137-
if (seenKeys.contains(configKey)) continue;
138-
seenKeys.add(configKey);
139-
140-
String nodeId = "config:" + configKey;
141-
CodeNode node = new CodeNode();
142-
node.setId(nodeId);
143-
node.setKind(NodeKind.CONFIG_DEFINITION);
144-
node.setLabel(configKey);
145-
node.setFilePath(ctx.filePath());
146-
node.setLineStart(i + 1);
147-
node.getProperties().put("config_key", configKey);
148-
nodes.add(node);
149-
150-
CodeEdge edge = new CodeEdge();
151-
edge.setId(classNodeId + "->reads_config->" + nodeId);
152-
edge.setKind(EdgeKind.READS_CONFIG);
153-
edge.setSourceId(classNodeId);
154-
edge.setTarget(node);
155-
edge.setProperties(Map.of("config_key", configKey));
156-
edges.add(edge);
204+
if (m.find()) {
205+
String configKey = m.group(1);
206+
if (seenKeys.add(configKey)) {
207+
addConfigNode(configKey, "kafka_configdef", classNodeId, ctx.filePath(), i + 1, nodes, edges);
208+
}
209+
}
210+
211+
// Spring @Value("${...}")
212+
Matcher vm = VALUE_RE.matcher(lines[i]);
213+
while (vm.find()) {
214+
String key = vm.group(1);
215+
if (seenKeys.add(key)) {
216+
addConfigNode(key, "spring_value", classNodeId, ctx.filePath(), i + 1, nodes, edges);
217+
}
218+
}
219+
220+
// Spring @ConfigurationProperties("prefix")
221+
Matcher cpm = CONFIG_PROPS_RE.matcher(lines[i]);
222+
if (cpm.find()) {
223+
String prefix = cpm.group(1);
224+
if (seenKeys.add(prefix)) {
225+
addConfigNode(prefix, "spring_config_props", classNodeId, ctx.filePath(), i + 1, nodes, edges);
226+
}
227+
}
157228
}
158229

159230
return DetectorResult.of(nodes, edges);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
name = "kafka",
2222
category = "messaging",
2323
description = "Detects Kafka producers, consumers, and topic configurations",
24-
languages = {"java"},
24+
languages = {"java", "kotlin"},
2525
nodeKinds = {NodeKind.TOPIC},
2626
edgeKinds = {EdgeKind.CONSUMES, EdgeKind.PRODUCES},
2727
properties = {"broker", "group_id", "topic"}
@@ -43,7 +43,7 @@ public String getName() {
4343

4444
@Override
4545
public Set<String> getSupportedLanguages() {
46-
return Set.of("java");
46+
return Set.of("java", "kotlin");
4747
}
4848

4949
@Override

0 commit comments

Comments
 (0)