Skip to content

Commit f87b212

Browse files
fix(detection): backport CTO-reviewed detector fixes into phase5 branch
1. ConfigDefDetector: move seenKeys to file scope to prevent duplicate config nodes when multiple classes in the same file reference the same @value or ConfigDef key. 2. ConfigDefDetector: add receiver discriminator to .define() call matching — only flags calls where the scope references "ConfigDef" or "CONFIG" to prevent false positives from unrelated .define() calls. 3. KafkaDetector: extend CLASS_RE to include enum|inline|value|protected Kotlin modifiers (enum class, inline class, value class, protected class). 4. TopicLinker: extend to handle PUBLISHES/LISTENS edge kinds and TOPIC/QUEUE/EVENT/MESSAGE_QUEUE node kinds for full messaging coverage. 5. TopicLinker: add determinism test covering multi-sender/multi-receiver SENDS_TO/RECEIVES_FROM scenarios. These fixes were reviewed and approved in the detection-quality-fixes branch (46fd2b0) but were absent from the phase5 branch. Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 parent 59481af commit f87b212

8 files changed

Lines changed: 741 additions & 12 deletions

File tree

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

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

2020
/**
21-
* Links messaging producers to consumers via shared topic/queue names.
21+
* Links messaging producers to consumers via shared topic/queue/event names.
2222
* <p>
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.
23+
* Scans for TOPIC, QUEUE, EVENT, and MESSAGE_QUEUE nodes and matches producer
24+
* edges (PRODUCES, SENDS_TO, PUBLISHES) with consumer edges (CONSUMES,
25+
* RECEIVES_FROM, LISTENS) on the same label to create direct producer-to-consumer
26+
* CALLS edges. Supports Kafka, RabbitMQ, TIBCO EMS, IBM MQ, Azure Service Bus,
27+
* Spring application events, and other enterprise messaging patterns.
2728
*/
2829
@Component
2930
public class TopicLinker implements Linker {
@@ -35,7 +36,8 @@ public LinkResult link(List<CodeNode> nodes, List<CodeEdge> edges) {
3536
// Collect topic/queue nodes by label
3637
Map<String, List<String>> topicIdsByLabel = new TreeMap<>();
3738
for (CodeNode node : nodes) {
38-
if (node.getKind() == NodeKind.TOPIC || node.getKind() == NodeKind.QUEUE) {
39+
if (node.getKind() == NodeKind.TOPIC || node.getKind() == NodeKind.QUEUE
40+
|| node.getKind() == NodeKind.EVENT || node.getKind() == NodeKind.MESSAGE_QUEUE) {
3941
topicIdsByLabel
4042
.computeIfAbsent(node.getLabel(), k -> new ArrayList<>())
4143
.add(node.getId());
@@ -52,8 +54,10 @@ public LinkResult link(List<CodeNode> nodes, List<CodeEdge> edges) {
5254
Map<String, List<String>> consumersByTopic = new TreeMap<>();
5355

5456
for (CodeEdge edge : edges) {
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+
boolean isProducer = edge.getKind() == EdgeKind.PRODUCES || edge.getKind() == EdgeKind.SENDS_TO
58+
|| edge.getKind() == EdgeKind.PUBLISHES;
59+
boolean isConsumer = edge.getKind() == EdgeKind.CONSUMES || edge.getKind() == EdgeKind.RECEIVES_FROM
60+
|| edge.getKind() == EdgeKind.LISTENS;
5761
if (isProducer && edge.getTarget() != null) {
5862
producersByTopic
5963
.computeIfAbsent(edge.getTarget().getId(), k -> new ArrayList<>())

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,28 @@ public DetectorResult detect(DetectorContext ctx) {
8080
private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) {
8181
List<CodeNode> nodes = new ArrayList<>();
8282
List<CodeEdge> edges = new ArrayList<>();
83+
// File-scoped seenKeys prevents duplicate config nodes when multiple classes
84+
// in the same file reference the same key (e.g., @Value("${server.port}")).
85+
Set<String> seenKeys = new LinkedHashSet<>();
8386

8487
cu.findAll(ClassOrInterfaceDeclaration.class).forEach(classDecl -> {
8588
String className = classDecl.getNameAsString();
8689
String classNodeId = ctx.filePath() + ":" + className;
87-
Set<String> seenKeys = new LinkedHashSet<>();
8890

8991
// 1. Kafka ConfigDef.define() calls
92+
// Discriminator: receiver must mention "ConfigDef" or "CONFIG" to avoid matching
93+
// unrelated .define() calls (per CLAUDE.md: framework detectors must have guards).
9094
classDecl.findAll(MethodCallExpr.class).forEach(call -> {
9195
if (!"define".equals(call.getNameAsString())) return;
9296
if (call.getArguments().isEmpty()) return;
97+
// Receiver check: scope must reference a ConfigDef instance or chain
98+
boolean receiverIsConfigDef = call.getScope()
99+
.map(scope -> {
100+
String s = scope.toString();
101+
return s.contains("ConfigDef") || s.toUpperCase().contains("CONFIG");
102+
})
103+
.orElse(false);
104+
if (!receiverIsConfigDef) return;
93105
var firstArg = call.getArguments().get(0);
94106
if (!firstArg.isStringLiteralExpr()) return;
95107
String configKey = firstArg.asStringLiteralExpr().getValue();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
public class KafkaDetector extends AbstractRegexDetector {
3131

3232
private static final Pattern CLASS_RE = Pattern.compile(
33-
"(?:(?:public|internal|private|abstract|sealed|data|open)\\s+)*(?:class|object)\\s+(\\w+)");
33+
"(?:(?:public|internal|private|protected|data|abstract|open|sealed|enum|inline|value)\\s+)*" +
34+
"(?:class|object)\\s+(\\w+)");
3435
private static final Pattern KAFKA_LISTENER_RE = Pattern.compile(
3536
"@KafkaListener\\s*\\(\\s*(?:.*?topics?\\s*=\\s*)?[\\{\"]?\\s*\"([^\"]+)\"");
3637
private static final Pattern KAFKA_SEND_RE = Pattern.compile(

src/main/resources/static/assets/CodeGraphView-Bs3FzZUD.js

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/resources/static/assets/index-C1nABH5f.js

Lines changed: 318 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/resources/static/assets/index-Gcxm3PDg.js

Lines changed: 204 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/resources/static/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
<link rel="preconnect" href="https://fonts.googleapis.com" />
1010
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1111
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
12-
<script type="module" crossorigin src="/assets/index-DNHxJZLD.js"></script>
13-
<link rel="modulepreload" crossorigin href="/assets/vendor-react-74ka7VJT.js">
12+
<script type="module" crossorigin src="/assets/index-C1nABH5f.js"></script>
13+
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DGy_UPuR.js">
1414
<link rel="stylesheet" crossorigin href="/assets/index-BOVJBvRE.css">
1515
</head>
1616
<body class="bg-surface-950 text-surface-100">

src/test/java/io/github/randomcodespace/iq/analyzer/linker/TopicLinkerTest.java

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,130 @@ void linksMixedSendsToAndConsumes() {
136136
assertEquals(EdgeKind.CALLS, result.edges().getFirst().getKind());
137137
}
138138

139+
@Test
140+
void linksPublisherToListenerViaEvent() {
141+
var event = new CodeNode("event:UserCreated", NodeKind.EVENT, "UserCreated");
142+
var publisher = new CodeNode("svc:UserService", NodeKind.CLASS, "UserService");
143+
var listener = new CodeNode("svc:NotificationService", NodeKind.CLASS, "NotificationService");
144+
145+
var publishesEdge = new CodeEdge();
146+
publishesEdge.setId("e1");
147+
publishesEdge.setKind(EdgeKind.PUBLISHES);
148+
publishesEdge.setSourceId("svc:UserService");
149+
publishesEdge.setTarget(event);
150+
151+
var listensEdge = new CodeEdge();
152+
listensEdge.setId("e2");
153+
listensEdge.setKind(EdgeKind.LISTENS);
154+
listensEdge.setSourceId("svc:NotificationService");
155+
listensEdge.setTarget(event);
156+
157+
LinkResult result = linker.link(
158+
List.of(event, publisher, listener),
159+
List.of(publishesEdge, listensEdge)
160+
);
161+
162+
assertEquals(1, result.edges().size());
163+
CodeEdge callsEdge = result.edges().getFirst();
164+
assertEquals(EdgeKind.CALLS, callsEdge.getKind());
165+
assertEquals("svc:UserService", callsEdge.getSourceId());
166+
assertEquals("svc:NotificationService", callsEdge.getTarget().getId());
167+
assertEquals("UserCreated", callsEdge.getProperties().get("topic"));
168+
}
169+
170+
@Test
171+
void linksPublisherWithExistingConsumer() {
172+
var event = new CodeNode("event:OrderPlaced", NodeKind.EVENT, "OrderPlaced");
173+
var publisher = new CodeNode("svc:OrderService", NodeKind.CLASS, "OrderService");
174+
var consumer = new CodeNode("svc:InventoryService", NodeKind.CLASS, "InventoryService");
175+
176+
var publishesEdge = new CodeEdge();
177+
publishesEdge.setId("e1");
178+
publishesEdge.setKind(EdgeKind.PUBLISHES);
179+
publishesEdge.setSourceId("svc:OrderService");
180+
publishesEdge.setTarget(event);
181+
182+
var consumesEdge = new CodeEdge();
183+
consumesEdge.setId("e2");
184+
consumesEdge.setKind(EdgeKind.CONSUMES);
185+
consumesEdge.setSourceId("svc:InventoryService");
186+
consumesEdge.setTarget(event);
187+
188+
LinkResult result = linker.link(
189+
List.of(event, publisher, consumer),
190+
List.of(publishesEdge, consumesEdge)
191+
);
192+
193+
assertEquals(1, result.edges().size());
194+
assertEquals(EdgeKind.CALLS, result.edges().getFirst().getKind());
195+
}
196+
197+
@Test
198+
void handlesMessageQueueNodes() {
199+
var mq = new CodeNode("mq:ibm-orders", NodeKind.MESSAGE_QUEUE, "ibm-orders");
200+
var producer = new CodeNode("svc:IBMProducer", NodeKind.CLASS, "IBMProducer");
201+
var consumer = new CodeNode("svc:IBMConsumer", NodeKind.CLASS, "IBMConsumer");
202+
203+
var sendsEdge = new CodeEdge();
204+
sendsEdge.setId("e1");
205+
sendsEdge.setKind(EdgeKind.SENDS_TO);
206+
sendsEdge.setSourceId("svc:IBMProducer");
207+
sendsEdge.setTarget(mq);
208+
209+
var receivesEdge = new CodeEdge();
210+
receivesEdge.setId("e2");
211+
receivesEdge.setKind(EdgeKind.RECEIVES_FROM);
212+
receivesEdge.setSourceId("svc:IBMConsumer");
213+
receivesEdge.setTarget(mq);
214+
215+
LinkResult result = linker.link(
216+
List.of(mq, producer, consumer),
217+
List.of(sendsEdge, receivesEdge)
218+
);
219+
220+
assertEquals(1, result.edges().size());
221+
assertEquals(EdgeKind.CALLS, result.edges().getFirst().getKind());
222+
}
223+
224+
@Test
225+
void determinismTest() {
226+
var topic = new CodeNode("topic:payments", NodeKind.TOPIC, "payments");
227+
var event = new CodeNode("event:PaymentProcessed", NodeKind.EVENT, "PaymentProcessed");
228+
var svcA = new CodeNode("svc:A", NodeKind.CLASS, "A");
229+
var svcB = new CodeNode("svc:B", NodeKind.CLASS, "B");
230+
var svcC = new CodeNode("svc:C", NodeKind.CLASS, "C");
231+
232+
List<CodeEdge> edges = new ArrayList<>();
233+
234+
var e1 = new CodeEdge();
235+
e1.setId("e1"); e1.setKind(EdgeKind.PRODUCES); e1.setSourceId("svc:A"); e1.setTarget(topic);
236+
edges.add(e1);
237+
238+
var e2 = new CodeEdge();
239+
e2.setId("e2"); e2.setKind(EdgeKind.CONSUMES); e2.setSourceId("svc:B"); e2.setTarget(topic);
240+
edges.add(e2);
241+
242+
var e3 = new CodeEdge();
243+
e3.setId("e3"); e3.setKind(EdgeKind.PUBLISHES); e3.setSourceId("svc:A"); e3.setTarget(event);
244+
edges.add(e3);
245+
246+
var e4 = new CodeEdge();
247+
e4.setId("e4"); e4.setKind(EdgeKind.LISTENS); e4.setSourceId("svc:C"); e4.setTarget(event);
248+
edges.add(e4);
249+
250+
List<CodeNode> nodes = List.of(topic, event, svcA, svcB, svcC);
251+
252+
LinkResult result1 = linker.link(nodes, edges);
253+
LinkResult result2 = linker.link(nodes, edges);
254+
255+
assertEquals(result1.edges().size(), result2.edges().size());
256+
for (int i = 0; i < result1.edges().size(); i++) {
257+
assertEquals(result1.edges().get(i).getId(), result2.edges().get(i).getId());
258+
assertEquals(result1.edges().get(i).getSourceId(), result2.edges().get(i).getSourceId());
259+
assertEquals(result1.edges().get(i).getTarget().getId(), result2.edges().get(i).getTarget().getId());
260+
}
261+
}
262+
139263
@Test
140264
void handlesQueueNodes() {
141265
var queue = new CodeNode("queue:tasks", NodeKind.QUEUE, "tasks");

0 commit comments

Comments
 (0)