Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
* Links Kafka/RabbitMQ producers to consumers via shared topic names.
* Links messaging producers to consumers via shared topic/queue/event names.
* <p>
* Scans for TOPIC/QUEUE nodes and matches PRODUCES edges with CONSUMES
* edges on the same topic label to create direct producer-to-consumer
* Scans for TOPIC/QUEUE/EVENT/MESSAGE_QUEUE nodes and matches producer edges
* (PRODUCES, SENDS_TO, PUBLISHES) with consumer edges (CONSUMES, RECEIVES_FROM,
* LISTENS) on the same topic label to create direct producer-to-consumer
* CALLS edges.
*/
@Component
Expand All @@ -30,11 +30,12 @@
private static final Logger log = LoggerFactory.getLogger(TopicLinker.class);

@Override
public LinkResult link(List<CodeNode> nodes, List<CodeEdge> edges) {

Check warning on line 33 in src/main/java/io/github/randomcodespace/iq/analyzer/linker/TopicLinker.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 69 to 64, Complexity from 25 to 14, Nesting Level from 5 to 2, Number of Variables from 26 to 6.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1KFXeB_cRLGSbPkaJb&open=AZ1KFXeB_cRLGSbPkaJb&pullRequest=16
// Collect topic/queue nodes by label
// Collect topic/queue/event/message_queue nodes by label
Map<String, List<String>> topicIdsByLabel = new TreeMap<>();
for (CodeNode node : nodes) {
if (node.getKind() == NodeKind.TOPIC || node.getKind() == NodeKind.QUEUE) {
if (node.getKind() == NodeKind.TOPIC || node.getKind() == NodeKind.QUEUE
|| node.getKind() == NodeKind.EVENT || node.getKind() == NodeKind.MESSAGE_QUEUE) {
topicIdsByLabel
.computeIfAbsent(node.getLabel(), k -> new ArrayList<>())
.add(node.getId());
Expand All @@ -51,11 +52,13 @@
Map<String, List<String>> consumersByTopic = new TreeMap<>();

for (CodeEdge edge : edges) {
if (edge.getKind() == EdgeKind.PRODUCES && edge.getTarget() != null) {
if (edge.getTarget() == null) continue;
EdgeKind kind = edge.getKind();
if (kind == EdgeKind.PRODUCES || kind == EdgeKind.SENDS_TO || kind == EdgeKind.PUBLISHES) {
producersByTopic
.computeIfAbsent(edge.getTarget().getId(), k -> new ArrayList<>())
.add(edge.getSourceId());
} else if (edge.getKind() == EdgeKind.CONSUMES && edge.getTarget() != null) {
} else if (kind == EdgeKind.CONSUMES || kind == EdgeKind.RECEIVES_FROM || kind == EdgeKind.LISTENS) {
consumersByTopic
.computeIfAbsent(edge.getTarget().getId(), k -> new ArrayList<>())
.add(edge.getSourceId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,14 @@ private int enrichFromCache(AnalysisCache cache, Path root, NumberFormat nf, Ins
tx.execute("CREATE INDEX IF NOT EXISTS FOR (n:CodeNode) ON (n.filePath)");
tx.execute("CREATE INDEX IF NOT EXISTS FOR (n:CodeNode) ON (n.label_lower)");
tx.execute("CREATE INDEX IF NOT EXISTS FOR (n:CodeNode) ON (n.fqn_lower)");
tx.execute("CREATE FULLTEXT INDEX search_index IF NOT EXISTS "
+ "FOR (n:CodeNode) ON EACH [n.label_lower, n.fqn_lower] "
+ "OPTIONS {indexConfig: {`fulltext.analyzer`: 'keyword'}}");
tx.commit();
}
// Wait for all indexes (including fulltext) to finish building
try (Transaction tx = db.beginTx()) {
tx.execute("CALL db.awaitIndexes(300)");
tx.commit();
}
CliOutput.info(" Created Neo4j indexes");
Expand Down
41 changes: 35 additions & 6 deletions src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
* Creates an index on CodeNode.id for fast MATCH during edge creation.
* Logs progress every 10K items for visibility on large graphs.
*/
public void bulkSave(List<CodeNode> nodes) {

Check warning on line 71 in src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 87 to 64, Complexity from 19 to 14, Nesting Level from 3 to 2, Number of Variables from 29 to 6.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1KFXia_cRLGSbPkaJf&open=AZ1KFXia_cRLGSbPkaJf&pullRequest=16
if (nodes.isEmpty()) return;
long start = System.currentTimeMillis();

Expand All @@ -89,6 +89,9 @@
tx.execute("CREATE INDEX IF NOT EXISTS FOR (n:CodeNode) ON (n.id)");
tx.execute("CREATE INDEX IF NOT EXISTS FOR (n:CodeNode) ON (n.label_lower)");
tx.execute("CREATE INDEX IF NOT EXISTS FOR (n:CodeNode) ON (n.fqn_lower)");
tx.execute("CREATE FULLTEXT INDEX search_index IF NOT EXISTS "
+ "FOR (n:CodeNode) ON EACH [n.label_lower, n.fqn_lower] "
+ "OPTIONS {indexConfig: {`fulltext.analyzer`: 'keyword'}}");
tx.commit();
}

Expand Down Expand Up @@ -170,7 +173,7 @@
}

/** Convert a CodeNode to a flat property map for Cypher SET. */
private Map<String, Object> nodeToProps(CodeNode node) {

Check failure on line 176 in src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1KFXia_cRLGSbPkaJe&open=AZ1KFXia_cRLGSbPkaJe&pullRequest=16
Map<String, Object> props = new HashMap<>();
props.put("id", node.getId());
props.put("kind", node.getKind().getValue());
Expand Down Expand Up @@ -249,16 +252,42 @@

public List<CodeNode> search(String text) {
return queryNodes(
"MATCH (n:CodeNode) WHERE n.label CONTAINS $text OR n.fqn CONTAINS $text RETURN n",
Map.of("text", text));
"CALL db.index.fulltext.queryNodes('search_index', $text) "
+ "YIELD node RETURN node AS n",
Map.of("text", toLuceneQuery(text)));
}

public List<CodeNode> search(String text, int limit) {
String lowerText = text.toLowerCase();
return queryNodes(
"MATCH (n:CodeNode) WHERE n.label_lower CONTAINS $text "
+ "OR n.fqn_lower CONTAINS $text RETURN n LIMIT $limit",
Map.of("text", lowerText, "limit", limit));
"CALL db.index.fulltext.queryNodes('search_index', $text) "
+ "YIELD node RETURN node AS n LIMIT $limit",
Map.of("text", toLuceneQuery(text), "limit", limit));

Check failure on line 264 in src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "limit" 8 times.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1KFXia_cRLGSbPkaJd&open=AZ1KFXia_cRLGSbPkaJd&pullRequest=16
}

/**
* Wraps a search term in Lucene wildcard syntax for substring matching against
* the fulltext index (which stores lowercased property values via keyword analyzer).
* Escapes Lucene special characters before wrapping.
*/
private static String toLuceneQuery(String text) {
String lower = text.toLowerCase();
String escaped = lower
.replace("\\", "\\\\")
.replace("+", "\\+")
.replace("-", "\\-")
.replace("!", "\\!")
.replace("(", "\\(")
.replace(")", "\\)")
.replace("{", "\\{")
.replace("}", "\\}")
.replace("[", "\\[")
.replace("]", "\\]")
.replace("^", "\\^")
.replace("\"", "\\\"")
.replace("~", "\\~")
.replace(":", "\\:")
.replace("/", "\\/");
return "*" + escaped + "*";
}

public List<CodeNode> findNeighbors(String nodeId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,121 @@ void handlesQueueNodes() {

assertEquals(1, result.edges().size());
}

@Test
void linksSendsToReceivesFromEdgesViaTopic() {
// Tibco EMS / Azure Service Bus pattern
var topic = new CodeNode("topic:orders", NodeKind.TOPIC, "orders");
var producer = new CodeNode("svc:TibcoSender", NodeKind.CLASS, "TibcoSender");
var consumer = new CodeNode("svc:TibcoReceiver", NodeKind.CLASS, "TibcoReceiver");

var sendsToEdge = new CodeEdge();
sendsToEdge.setId("e1");
sendsToEdge.setKind(EdgeKind.SENDS_TO);
sendsToEdge.setSourceId("svc:TibcoSender");
sendsToEdge.setTarget(topic);

var receivesFromEdge = new CodeEdge();
receivesFromEdge.setId("e2");
receivesFromEdge.setKind(EdgeKind.RECEIVES_FROM);
receivesFromEdge.setSourceId("svc:TibcoReceiver");
receivesFromEdge.setTarget(topic);

LinkResult result = linker.link(
List.of(topic, producer, consumer),
List.of(sendsToEdge, receivesFromEdge)
);

assertEquals(1, result.edges().size());
CodeEdge callsEdge = result.edges().getFirst();
assertEquals(EdgeKind.CALLS, callsEdge.getKind());
assertEquals("svc:TibcoSender", callsEdge.getSourceId());
assertEquals("svc:TibcoReceiver", callsEdge.getTarget().getId());
assertEquals(true, callsEdge.getProperties().get("inferred"));
}

@Test
void linksPublishesListensEdgesViaEventNode() {
// Spring Events pattern using EVENT node kind
var event = new CodeNode("event:UserCreated", NodeKind.EVENT, "UserCreated");
var publisher = new CodeNode("svc:UserService", NodeKind.CLASS, "UserService");
var listener = new CodeNode("svc:EmailService", NodeKind.CLASS, "EmailService");

var publishesEdge = new CodeEdge();
publishesEdge.setId("e1");
publishesEdge.setKind(EdgeKind.PUBLISHES);
publishesEdge.setSourceId("svc:UserService");
publishesEdge.setTarget(event);

var listensEdge = new CodeEdge();
listensEdge.setId("e2");
listensEdge.setKind(EdgeKind.LISTENS);
listensEdge.setSourceId("svc:EmailService");
listensEdge.setTarget(event);

LinkResult result = linker.link(
List.of(event, publisher, listener),
List.of(publishesEdge, listensEdge)
);

assertEquals(1, result.edges().size());
CodeEdge callsEdge = result.edges().getFirst();
assertEquals(EdgeKind.CALLS, callsEdge.getKind());
assertEquals("svc:UserService", callsEdge.getSourceId());
assertEquals("svc:EmailService", callsEdge.getTarget().getId());
assertEquals("UserCreated", callsEdge.getProperties().get("topic"));
}

@Test
void handlesMessageQueueNodeKind() {
var mq = new CodeNode("mq:notifications", NodeKind.MESSAGE_QUEUE, "notifications");
var sender = new CodeNode("svc:NotifySender", NodeKind.CLASS, "NotifySender");
var receiver = new CodeNode("svc:NotifyWorker", NodeKind.CLASS, "NotifyWorker");

var sendsEdge = new CodeEdge();
sendsEdge.setId("e1");
sendsEdge.setKind(EdgeKind.SENDS_TO);
sendsEdge.setSourceId("svc:NotifySender");
sendsEdge.setTarget(mq);

var receivesEdge = new CodeEdge();
receivesEdge.setId("e2");
receivesEdge.setKind(EdgeKind.RECEIVES_FROM);
receivesEdge.setSourceId("svc:NotifyWorker");
receivesEdge.setTarget(mq);

LinkResult result = linker.link(
List.of(mq, sender, receiver),
List.of(sendsEdge, receivesEdge)
);

assertEquals(1, result.edges().size());
}

@Test
void determinismTest() {
var topic = new CodeNode("topic:payments", NodeKind.TOPIC, "payments");
var prod1 = new CodeNode("svc:P1", NodeKind.CLASS, "P1");
var prod2 = new CodeNode("svc:P2", NodeKind.CLASS, "P2");
var cons = new CodeNode("svc:C1", NodeKind.CLASS, "C1");

var e1 = new CodeEdge();
e1.setId("e1"); e1.setKind(EdgeKind.PUBLISHES); e1.setSourceId("svc:P1"); e1.setTarget(topic);
var e2 = new CodeEdge();
e2.setId("e2"); e2.setKind(EdgeKind.SENDS_TO); e2.setSourceId("svc:P2"); e2.setTarget(topic);
var e3 = new CodeEdge();
e3.setId("e3"); e3.setKind(EdgeKind.LISTENS); e3.setSourceId("svc:C1"); e3.setTarget(topic);

List<CodeNode> nodeList = new ArrayList<>(List.of(topic, prod1, prod2, cons));
List<CodeEdge> edgeList = new ArrayList<>(List.of(e1, e2, e3));

LinkResult result1 = linker.link(nodeList, edgeList);
LinkResult result2 = linker.link(nodeList, edgeList);

assertEquals(result1.edges().size(), result2.edges().size());
for (int i = 0; i < result1.edges().size(); i++) {
assertEquals(result1.edges().get(i).getId(), result2.edges().get(i).getId());
assertEquals(result1.edges().get(i).getSourceId(), result2.edges().get(i).getSourceId());
}
}
}
Loading