From 963afa06b7e53b316707fdc7e4d85eb7fad290f6 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sat, 4 Apr 2026 15:38:04 +0000 Subject: [PATCH] refactor: extract within-file helpers to eliminate detectWithAst/detectWithRegex duplication Python auth/route detectors: - FastAPIAuthDetector: 5 helpers (createDependsGuard, createSecurityGuard, etc.) - DjangoAuthDetector: 4 helpers (createLoginRequiredGuard, etc.) - FastAPIRouteDetector: createRouteEndpoint helper - DjangoViewDetector: createUrlPatternEndpoint, createCbvNode helpers - FlaskRouteDetector: createFlaskRouteEndpoint, createExposesEdge helpers - CeleryTaskDetector: createQueueNode, createMethodNode, edge helpers Python entity detectors: - DjangoModelDetector: 7 helpers (createModelNode, extractMeta, addFkEdges, etc.) - SQLAlchemyModelDetector: 4 helpers (createEntityNode, extractColumns, etc.) - PydanticModelDetector: 5 helpers (createPydanticNode, extractConfigProps, etc.) - PythonStructuresDetector: 8 helpers + AllExports record Frontend detectors: - FrontendDetectorHelper: createComponentNode, lineAt shared helpers - Refactored Angular, React, Vue component detectors Java messaging detectors: - AbstractJavaMessagingDetector: CLASS_RE, extractClassName, addMessagingEdge - Refactored JMS, IBM MQ, Tibco EMS, RabbitMQ detectors Scala/Kotlin structures: - StructuresDetectorHelper: addImportEdge, createStructureNode, edge helpers Co-Authored-By: Claude Sonnet 4.6 --- .../iq/detector/StructuresDetectorHelper.java | 68 ++++ .../frontend/AngularComponentDetector.java | 55 +-- .../frontend/FrontendDetectorHelper.java | 44 +++ .../frontend/ReactComponentDetector.java | 34 +- .../frontend/VueComponentDetector.java | 45 +-- .../java/AbstractJavaMessagingDetector.java | 49 +++ .../iq/detector/java/IbmMqDetector.java | 28 +- .../iq/detector/java/JmsDetector.java | 40 +- .../iq/detector/java/RabbitmqDetector.java | 27 +- .../iq/detector/java/TibcoEmsDetector.java | 28 +- .../kotlin/KotlinStructuresDetector.java | 34 +- .../detector/python/CeleryTaskDetector.java | 117 +++--- .../detector/python/DjangoAuthDetector.java | 186 ++++------ .../detector/python/DjangoModelDetector.java | 308 +++++++--------- .../detector/python/DjangoViewDetector.java | 110 +++--- .../detector/python/FastAPIAuthDetector.java | 235 +++++------- .../detector/python/FastAPIRouteDetector.java | 51 ++- .../detector/python/FlaskRouteDetector.java | 74 ++-- .../python/PydanticModelDetector.java | 214 +++++------ .../python/PythonStructuresDetector.java | 347 +++++++----------- .../python/SQLAlchemyModelDetector.java | 120 +++--- .../scala/ScalaStructuresDetector.java | 37 +- 22 files changed, 940 insertions(+), 1311 deletions(-) create mode 100644 src/main/java/io/github/randomcodespace/iq/detector/StructuresDetectorHelper.java create mode 100644 src/main/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorHelper.java create mode 100644 src/main/java/io/github/randomcodespace/iq/detector/java/AbstractJavaMessagingDetector.java diff --git a/src/main/java/io/github/randomcodespace/iq/detector/StructuresDetectorHelper.java b/src/main/java/io/github/randomcodespace/iq/detector/StructuresDetectorHelper.java new file mode 100644 index 00000000..45087871 --- /dev/null +++ b/src/main/java/io/github/randomcodespace/iq/detector/StructuresDetectorHelper.java @@ -0,0 +1,68 @@ +package io.github.randomcodespace.iq.detector; + +import io.github.randomcodespace.iq.model.CodeEdge; +import io.github.randomcodespace.iq.model.CodeNode; +import io.github.randomcodespace.iq.model.EdgeKind; +import io.github.randomcodespace.iq.model.NodeKind; + +import java.util.List; + +/** + * Shared helper methods for language structure detectors (Scala, Kotlin, etc.) + * that detect classes, interfaces, objects, methods, and imports using regex. + */ +public final class StructuresDetectorHelper { + + private StructuresDetectorHelper() {} + + /** + * Create and add an IMPORTS edge. + */ + public static void addImportEdge(String filePath, String target, List edges) { + CodeEdge e = new CodeEdge(); + e.setId(filePath + ":imports:" + target); + e.setKind(EdgeKind.IMPORTS); + e.setSourceId(filePath); + e.setTarget(new CodeNode(target, NodeKind.MODULE, target)); + edges.add(e); + } + + /** + * Create a code node with standard fields for structure detection. + */ + public static CodeNode createStructureNode(String filePath, String name, NodeKind kind, int lineStart) { + CodeNode n = new CodeNode(); + n.setId(filePath + ":" + name); + n.setKind(kind); + n.setLabel(name); + n.setFqn(name); + n.setFilePath(filePath); + n.setLineStart(lineStart); + return n; + } + + /** + * Create and add an EXTENDS edge. + */ + public static void addExtendsEdge(String sourceNodeId, String targetName, NodeKind targetKind, + List edges) { + CodeEdge e = new CodeEdge(); + e.setId(sourceNodeId + ":extends:" + targetName); + e.setKind(EdgeKind.EXTENDS); + e.setSourceId(sourceNodeId); + e.setTarget(new CodeNode(targetName, targetKind, targetName)); + edges.add(e); + } + + /** + * Create and add an IMPLEMENTS edge. + */ + public static void addImplementsEdge(String sourceNodeId, String targetName, List edges) { + CodeEdge e = new CodeEdge(); + e.setId(sourceNodeId + ":implements:" + targetName); + e.setKind(EdgeKind.IMPLEMENTS); + e.setSourceId(sourceNodeId); + e.setTarget(new CodeNode(targetName, NodeKind.INTERFACE, targetName)); + edges.add(e); + } +} diff --git a/src/main/java/io/github/randomcodespace/iq/detector/frontend/AngularComponentDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/frontend/AngularComponentDetector.java index 5864add5..9160d371 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/frontend/AngularComponentDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/frontend/AngularComponentDetector.java @@ -71,15 +71,8 @@ public DetectorResult detect(DetectorContext ctx) { String selector = m.group(1); String className = m.group(2); if (!seen.add(className)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("angular:" + filePath + ":component:" + className); - node.setKind(NodeKind.COMPONENT); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "angular"); + CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "component", + className, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("selector", selector); node.getProperties().put("decorator", "Component"); nodes.add(node); @@ -91,15 +84,8 @@ public DetectorResult detect(DetectorContext ctx) { String providedIn = m.group(1); String className = m.group(2); if (!seen.add(className)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("angular:" + filePath + ":service:" + className); - node.setKind(NodeKind.MIDDLEWARE); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "angular"); + CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "service", + className, NodeKind.MIDDLEWARE, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("provided_in", providedIn); node.getProperties().put("decorator", "Injectable"); nodes.add(node); @@ -111,15 +97,8 @@ public DetectorResult detect(DetectorContext ctx) { String selector = m.group(1); String className = m.group(2); if (!seen.add(className)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("angular:" + filePath + ":component:" + className); - node.setKind(NodeKind.COMPONENT); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "angular"); + CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "component", + className, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("selector", selector); node.getProperties().put("decorator", "Directive"); nodes.add(node); @@ -131,15 +110,8 @@ public DetectorResult detect(DetectorContext ctx) { String pipeName = m.group(1); String className = m.group(2); if (!seen.add(className)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("angular:" + filePath + ":component:" + className); - node.setKind(NodeKind.COMPONENT); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "angular"); + CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "component", + className, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("pipe_name", pipeName); node.getProperties().put("decorator", "Pipe"); nodes.add(node); @@ -150,15 +122,8 @@ public DetectorResult detect(DetectorContext ctx) { while (m.find()) { String className = m.group(1); if (!seen.add(className)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("angular:" + filePath + ":component:" + className); - node.setKind(NodeKind.COMPONENT); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "angular"); + CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "component", + className, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("decorator", "NgModule"); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorHelper.java b/src/main/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorHelper.java new file mode 100644 index 00000000..566c3765 --- /dev/null +++ b/src/main/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorHelper.java @@ -0,0 +1,44 @@ +package io.github.randomcodespace.iq.detector.frontend; + +import io.github.randomcodespace.iq.model.CodeNode; +import io.github.randomcodespace.iq.model.NodeKind; + +/** + * Shared helper methods for frontend component detectors (Angular, React, Vue). + * Provides common node creation patterns for components and hooks. + */ +final class FrontendDetectorHelper { + + private FrontendDetectorHelper() {} + + /** + * Create a frontend component node with standard fields. + * + * @param framework framework name (e.g. "angular", "react", "vue") + * @param filePath source file path + * @param idType node type for ID (e.g. "component", "service") + * @param className the component class/function name + * @param kind the node kind (COMPONENT, HOOK, MIDDLEWARE) + * @param line 1-based line number + * @return the configured CodeNode + */ + static CodeNode createComponentNode(String framework, String filePath, String idType, + String className, NodeKind kind, int line) { + CodeNode node = new CodeNode(); + node.setId(framework + ":" + filePath + ":" + idType + ":" + className); + node.setKind(kind); + node.setLabel(className); + node.setFqn(filePath + "::" + className); + node.setFilePath(filePath); + node.setLineStart(line); + node.getProperties().put("framework", framework); + return node; + } + + /** + * Calculate the 1-based line number from a character offset in text. + */ + static int lineAt(String text, int offset) { + return text.substring(0, offset).split("\n", -1).length; + } +} diff --git a/src/main/java/io/github/randomcodespace/iq/detector/frontend/ReactComponentDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/frontend/ReactComponentDetector.java index e04b65cf..bd87ddd8 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/frontend/ReactComponentDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/frontend/ReactComponentDetector.java @@ -67,16 +67,9 @@ record ComponentEntry(String name, String sourceId, int matchStart) {} while (m.find()) { String name = m.group(1); if (componentNames.contains(name)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; String sourceId = "react:" + filePath + ":component:" + name; - CodeNode node = new CodeNode(); - node.setId(sourceId); - node.setKind(NodeKind.COMPONENT); - node.setLabel(name); - node.setFqn(filePath + "::" + name); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "react"); + CodeNode node = FrontendDetectorHelper.createComponentNode("react", filePath, "component", + name, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("component_type", "function"); nodes.add(node); componentNames.add(name); @@ -90,16 +83,9 @@ record ComponentEntry(String name, String sourceId, int matchStart) {} while (m.find()) { String name = m.group(1); if (componentNames.contains(name)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; String sourceId = "react:" + filePath + ":component:" + name; - CodeNode node = new CodeNode(); - node.setId(sourceId); - node.setKind(NodeKind.COMPONENT); - node.setLabel(name); - node.setFqn(filePath + "::" + name); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "react"); + CodeNode node = FrontendDetectorHelper.createComponentNode("react", filePath, "component", + name, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("component_type", "class"); nodes.add(node); componentNames.add(name); @@ -114,16 +100,8 @@ record ComponentEntry(String name, String sourceId, int matchStart) {} while (m.find()) { String name = m.group(1); if (hookNames.contains(name)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("react:" + filePath + ":hook:" + name); - node.setKind(NodeKind.HOOK); - node.setLabel(name); - node.setFqn(filePath + "::" + name); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "react"); - nodes.add(node); + nodes.add(FrontendDetectorHelper.createComponentNode("react", filePath, "hook", + name, NodeKind.HOOK, FrontendDetectorHelper.lineAt(text, m.start()))); hookNames.add(name); } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/frontend/VueComponentDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/frontend/VueComponentDetector.java index 213fb211..8c8d85c0 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/frontend/VueComponentDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/frontend/VueComponentDetector.java @@ -68,15 +68,8 @@ public DetectorResult detect(DetectorContext ctx) { while (m.find()) { String name = m.group(1); if (componentNames.contains(name)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("vue:" + filePath + ":component:" + name); - node.setKind(NodeKind.COMPONENT); - node.setLabel(name); - node.setFqn(filePath + "::" + name); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "vue"); + CodeNode node = FrontendDetectorHelper.createComponentNode("vue", filePath, "component", + name, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("api_style", "composition"); nodes.add(node); componentNames.add(name); @@ -87,15 +80,8 @@ public DetectorResult detect(DetectorContext ctx) { while (m.find()) { String name = m.group(1); if (componentNames.contains(name)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("vue:" + filePath + ":component:" + name); - node.setKind(NodeKind.COMPONENT); - node.setLabel(name); - node.setFqn(filePath + "::" + name); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "vue"); + CodeNode node = FrontendDetectorHelper.createComponentNode("vue", filePath, "component", + name, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("api_style", "options"); nodes.add(node); componentNames.add(name); @@ -106,15 +92,8 @@ public DetectorResult detect(DetectorContext ctx) { while (m.find()) { String compName = extractScriptSetupName(filePath); if (compName == null || componentNames.contains(compName)) continue; - int line = text.substring(0, m.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("vue:" + filePath + ":component:" + compName); - node.setKind(NodeKind.COMPONENT); - node.setLabel(compName); - node.setFqn(filePath + "::" + compName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "vue"); + CodeNode node = FrontendDetectorHelper.createComponentNode("vue", filePath, "component", + compName, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("api_style", "script_setup"); nodes.add(node); componentNames.add(compName); @@ -127,16 +106,8 @@ public DetectorResult detect(DetectorContext ctx) { while (hm.find()) { String name = hm.group(1); if (hookNames.contains(name)) continue; - int line = text.substring(0, hm.start()).split("\n", -1).length; - CodeNode node = new CodeNode(); - node.setId("vue:" + filePath + ":hook:" + name); - node.setKind(NodeKind.HOOK); - node.setLabel(name); - node.setFqn(filePath + "::" + name); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "vue"); - nodes.add(node); + nodes.add(FrontendDetectorHelper.createComponentNode("vue", filePath, "hook", + name, NodeKind.HOOK, FrontendDetectorHelper.lineAt(text, hm.start()))); hookNames.add(name); } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/AbstractJavaMessagingDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/AbstractJavaMessagingDetector.java new file mode 100644 index 00000000..7ff27c03 --- /dev/null +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/AbstractJavaMessagingDetector.java @@ -0,0 +1,49 @@ +package io.github.randomcodespace.iq.detector.java; + +import io.github.randomcodespace.iq.detector.AbstractRegexDetector; +import io.github.randomcodespace.iq.model.CodeEdge; +import io.github.randomcodespace.iq.model.CodeNode; +import io.github.randomcodespace.iq.model.EdgeKind; +import io.github.randomcodespace.iq.model.NodeKind; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Shared base class for Java messaging detectors (JMS, RabbitMQ, IBM MQ, TIBCO EMS). + * Provides common patterns for class name extraction and edge creation. + */ +public abstract class AbstractJavaMessagingDetector extends AbstractRegexDetector { + + protected static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); + + /** + * Extract the first class name from the source text. + * Returns null if no class is found. + */ + protected String extractClassName(String text) { + String[] lines = text.split("\n", -1); + for (String line : lines) { + Matcher cm = CLASS_RE.matcher(line); + if (cm.find()) { + return cm.group(1); + } + } + return null; + } + + /** + * Create and add a messaging edge (CONSUMES, PRODUCES, SENDS_TO, RECEIVES_FROM, etc.). + */ + protected void addMessagingEdge(String sourceId, String targetId, EdgeKind kind, String label, + Map props, List edges) { + CodeEdge edge = new CodeEdge(); + edge.setId(sourceId + "->" + kind.getValue() + "->" + targetId); + edge.setKind(kind); + edge.setSourceId(sourceId); + edge.setTarget(new CodeNode(targetId, NodeKind.QUEUE, label)); + edge.setProperties(new LinkedHashMap<>(props)); + edges.add(edge); + } +} diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/IbmMqDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/IbmMqDetector.java index 9f913d38..9f9f298b 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/IbmMqDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/IbmMqDetector.java @@ -1,6 +1,5 @@ package io.github.randomcodespace.iq.detector.java; -import io.github.randomcodespace.iq.detector.AbstractRegexDetector; import io.github.randomcodespace.iq.detector.DetectorContext; import io.github.randomcodespace.iq.detector.DetectorResult; import io.github.randomcodespace.iq.model.CodeEdge; @@ -27,9 +26,8 @@ properties = {"broker", "queue", "topic"} ) @Component -public class IbmMqDetector extends AbstractRegexDetector { +public class IbmMqDetector extends AbstractJavaMessagingDetector { - private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); private static final Pattern QM_NEW_RE = Pattern.compile("new\\s+MQQueueManager\\s*\\(\\s*\"([^\"]+)\""); private static final Pattern ACCESS_QUEUE_RE = Pattern.compile("accessQueue\\s*\\(\\s*\"([^\"]+)\""); private static final Pattern MQ_TOPIC_DECL_RE = Pattern.compile("\\bMQTopic\\b"); @@ -62,11 +60,7 @@ public DetectorResult detect(DetectorContext ctx) { List nodes = new ArrayList<>(); List edges = new ArrayList<>(); - String className = null; - for (String line : lines) { - Matcher cm = CLASS_RE.matcher(line); - if (cm.find()) { className = cm.group(1); break; } - } + String className = extractClassName(text); if (className == null) return DetectorResult.empty(); String classNodeId = ctx.filePath() + ":" + className; @@ -85,7 +79,7 @@ public DetectorResult detect(DetectorContext ctx) { String qmId = ensureNode("ibmmq:qm:" + qmName, qmName, NodeKind.MESSAGE_QUEUE, "ibmmq:qm:" + qmName, Map.of("broker", "ibm_mq", "queue_manager", qmName), seenQms, nodes); - addEdge(classNodeId, qmId, EdgeKind.CONNECTS_TO, + addMessagingEdge(classNodeId, qmId, EdgeKind.CONNECTS_TO, className + " connects to queue manager " + qmName, Map.of("queue_manager", qmName), edges); } @@ -99,11 +93,11 @@ public DetectorResult detect(DetectorContext ctx) { String queueId = ensureNode("ibmmq:queue:" + queueName, queueName, NodeKind.QUEUE, "ibmmq:queue:" + queueName, Map.of("broker", "ibm_mq", "queue", queueName), seenQueues, nodes); - if (hasPut) addEdge(classNodeId, queueId, EdgeKind.SENDS_TO, + if (hasPut) addMessagingEdge(classNodeId, queueId, EdgeKind.SENDS_TO, className + " sends to " + queueName, Map.of("queue", queueName), edges); - if (hasGet) addEdge(classNodeId, queueId, EdgeKind.RECEIVES_FROM, + if (hasGet) addMessagingEdge(classNodeId, queueId, EdgeKind.RECEIVES_FROM, className + " receives from " + queueName, Map.of("queue", queueName), edges); - if (!hasPut && !hasGet) addEdge(classNodeId, queueId, EdgeKind.CONNECTS_TO, + if (!hasPut && !hasGet) addMessagingEdge(classNodeId, queueId, EdgeKind.CONNECTS_TO, className + " accesses " + queueName, Map.of("queue", queueName), edges); } } @@ -146,14 +140,4 @@ private String ensureNode(String id, String name, NodeKind kind, String label, return id; } - private void addEdge(String sourceId, String targetId, EdgeKind kind, String label, - Map props, List edges) { - CodeEdge edge = new CodeEdge(); - edge.setId(sourceId + "->" + kind.getValue() + "->" + targetId); - edge.setKind(kind); - edge.setSourceId(sourceId); - edge.setTarget(new CodeNode(targetId, NodeKind.QUEUE, label)); - edge.setProperties(new LinkedHashMap<>(props)); - edges.add(edge); - } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/JmsDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/JmsDetector.java index a59217cb..029c9577 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/JmsDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/JmsDetector.java @@ -1,6 +1,5 @@ package io.github.randomcodespace.iq.detector.java; -import io.github.randomcodespace.iq.detector.AbstractRegexDetector; import io.github.randomcodespace.iq.detector.DetectorContext; import io.github.randomcodespace.iq.detector.DetectorResult; import io.github.randomcodespace.iq.model.CodeEdge; @@ -27,9 +26,8 @@ properties = {"broker", "destination"} ) @Component -public class JmsDetector extends AbstractRegexDetector { +public class JmsDetector extends AbstractJavaMessagingDetector { - private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); private static final Pattern JMS_LISTENER_RE = Pattern.compile( "@JmsListener\\s*\\(\\s*(?:.*?destination\\s*=\\s*)?\"([^\"]+)\""); private static final Pattern JMS_SEND_RE = Pattern.compile( @@ -59,11 +57,7 @@ public DetectorResult detect(DetectorContext ctx) { List nodes = new ArrayList<>(); List edges = new ArrayList<>(); - String className = null; - for (String line : lines) { - Matcher cm = CLASS_RE.matcher(line); - if (cm.find()) { className = cm.group(1); break; } - } + String className = extractClassName(text); if (className == null) return DetectorResult.empty(); String classNodeId = ctx.filePath() + ":" + className; @@ -74,12 +68,12 @@ public DetectorResult detect(DetectorContext ctx) { Matcher m = JMS_LISTENER_RE.matcher(lines[i]); if (!m.find()) continue; String destination = m.group(1); - String queueId = ensureQueueNode(destination, seenQueues, nodes); + String queueId = ensureQueueNode("jms", destination, seenQueues, nodes); Map props = new LinkedHashMap<>(); props.put("destination", destination); Matcher cf = CONTAINER_FACTORY_RE.matcher(lines[i]); if (cf.find()) props.put("container_factory", cf.group(1)); - addEdge(classNodeId, queueId, EdgeKind.CONSUMES, + addMessagingEdge(classNodeId, queueId, EdgeKind.CONSUMES, className + " consumes from " + destination, props, edges); } @@ -88,8 +82,8 @@ public DetectorResult detect(DetectorContext ctx) { Matcher m = JMS_SEND_RE.matcher(lines[i]); if (!m.find()) continue; String destination = m.group(1); - String queueId = ensureQueueNode(destination, seenQueues, nodes); - addEdge(classNodeId, queueId, EdgeKind.PRODUCES, + String queueId = ensureQueueNode("jms", destination, seenQueues, nodes); + addMessagingEdge(classNodeId, queueId, EdgeKind.PRODUCES, className + " produces to " + destination, Map.of("destination", destination), edges); } @@ -97,29 +91,17 @@ public DetectorResult detect(DetectorContext ctx) { return DetectorResult.of(nodes, edges); } - private String ensureQueueNode(String destination, Set seen, List nodes) { - String queueId = "jms:queue:" + destination; - if (!seen.contains(destination)) { - seen.add(destination); + private String ensureQueueNode(String broker, String destination, Set seen, List nodes) { + String queueId = broker + ":queue:" + destination; + if (seen.add(destination)) { CodeNode node = new CodeNode(); node.setId(queueId); node.setKind(NodeKind.QUEUE); - node.setLabel("jms:" + destination); - node.getProperties().put("broker", "jms"); + node.setLabel(broker + ":" + destination); + node.getProperties().put("broker", broker); node.getProperties().put("destination", destination); nodes.add(node); } return queueId; } - - private void addEdge(String sourceId, String targetId, EdgeKind kind, String label, - Map props, List edges) { - CodeEdge edge = new CodeEdge(); - edge.setId(sourceId + "->" + kind.getValue() + "->" + targetId); - edge.setKind(kind); - edge.setSourceId(sourceId); - edge.setTarget(new CodeNode(targetId, NodeKind.QUEUE, label)); - edge.setProperties(new LinkedHashMap<>(props)); - edges.add(edge); - } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/RabbitmqDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/RabbitmqDetector.java index df8f0586..a32cc676 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/RabbitmqDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/RabbitmqDetector.java @@ -1,6 +1,5 @@ package io.github.randomcodespace.iq.detector.java; -import io.github.randomcodespace.iq.detector.AbstractRegexDetector; import io.github.randomcodespace.iq.detector.DetectorContext; import io.github.randomcodespace.iq.detector.DetectorResult; import io.github.randomcodespace.iq.model.CodeEdge; @@ -27,9 +26,8 @@ properties = {"broker", "exchange", "queue", "routing_key"} ) @Component -public class RabbitmqDetector extends AbstractRegexDetector { +public class RabbitmqDetector extends AbstractJavaMessagingDetector { - private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); private static final Pattern RABBIT_LISTENER_RE = Pattern.compile( "@RabbitListener\\s*\\(\\s*(?:.*?queues?\\s*=\\s*)?[\\{\"]?\\s*\"([^\"]+)\""); private static final Pattern RABBIT_SEND_RE = Pattern.compile( @@ -62,11 +60,7 @@ public DetectorResult detect(DetectorContext ctx) { List nodes = new ArrayList<>(); List edges = new ArrayList<>(); - String className = null; - for (String line : lines) { - Matcher cm = CLASS_RE.matcher(line); - if (cm.find()) { className = cm.group(1); break; } - } + String className = extractClassName(text); if (className == null) return DetectorResult.empty(); String classNodeId = ctx.filePath() + ":" + className; @@ -78,13 +72,8 @@ public DetectorResult detect(DetectorContext ctx) { if (!m.find()) continue; String queue = m.group(1); String queueId = ensureQueueNode(queue, seenQueues, nodes); - CodeEdge edge = new CodeEdge(); - edge.setId(classNodeId + "->consumes->" + queueId); - edge.setKind(EdgeKind.CONSUMES); - edge.setSourceId(classNodeId); - edge.setTarget(new CodeNode(queueId, NodeKind.QUEUE, queue)); - edge.setProperties(Map.of("queue", queue)); - edges.add(edge); + addMessagingEdge(classNodeId, queueId, EdgeKind.CONSUMES, queue, + Map.of("queue", queue), edges); } // RabbitTemplate sends @@ -109,13 +98,7 @@ public DetectorResult detect(DetectorContext ctx) { nodes.add(node); } - CodeEdge edge = new CodeEdge(); - edge.setId(classNodeId + "->produces->" + queueId); - edge.setKind(EdgeKind.PRODUCES); - edge.setSourceId(classNodeId); - edge.setTarget(new CodeNode(queueId, NodeKind.QUEUE, exchangeOrQueue)); - edge.setProperties(props); - edges.add(edge); + addMessagingEdge(classNodeId, queueId, EdgeKind.PRODUCES, exchangeOrQueue, props, edges); } // Exchange declarations diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/TibcoEmsDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/TibcoEmsDetector.java index e9e03798..475523b6 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/TibcoEmsDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/TibcoEmsDetector.java @@ -1,6 +1,5 @@ package io.github.randomcodespace.iq.detector.java; -import io.github.randomcodespace.iq.detector.AbstractRegexDetector; import io.github.randomcodespace.iq.detector.DetectorContext; import io.github.randomcodespace.iq.detector.DetectorResult; import io.github.randomcodespace.iq.model.CodeEdge; @@ -27,9 +26,8 @@ properties = {"broker", "queue", "topic"} ) @Component -public class TibcoEmsDetector extends AbstractRegexDetector { +public class TibcoEmsDetector extends AbstractJavaMessagingDetector { - private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); private static final Pattern TIBJMS_FACTORY_RE = Pattern.compile( "\\b(TibjmsConnectionFactory|TibjmsQueueConnectionFactory|TibjmsTopicConnectionFactory)\\b"); private static final Pattern SERVER_URL_RE = Pattern.compile("\"(tcp://[^\"]+)\""); @@ -68,11 +66,7 @@ public DetectorResult detect(DetectorContext ctx) { List nodes = new ArrayList<>(); List edges = new ArrayList<>(); - String className = null; - for (String line : lines) { - Matcher cm = CLASS_RE.matcher(line); - if (cm.find()) { className = cm.group(1); break; } - } + String className = extractClassName(text); if (className == null) return DetectorResult.empty(); String classNodeId = ctx.filePath() + ":" + className; @@ -121,18 +115,18 @@ public DetectorResult detect(DetectorContext ctx) { if (m.find()) { String queueName = m.group(1); String queueId = ensureQueueNode(queueName, seenQueues, nodes); - if (isProducer) addEdge(classNodeId, queueId, EdgeKind.SENDS_TO, + if (isProducer) addMessagingEdge(classNodeId, queueId, EdgeKind.SENDS_TO, className + " sends to " + queueName, Map.of("queue", queueName), edges); - if (isConsumer) addEdge(classNodeId, queueId, EdgeKind.RECEIVES_FROM, + if (isConsumer) addMessagingEdge(classNodeId, queueId, EdgeKind.RECEIVES_FROM, className + " receives from " + queueName, Map.of("queue", queueName), edges); } m = CREATE_TOPIC_RE.matcher(lines[i]); if (m.find()) { String topicName = m.group(1); String topicId = ensureTopicNode(topicName, seenTopics, nodes); - if (isProducer) addEdge(classNodeId, topicId, EdgeKind.SENDS_TO, + if (isProducer) addMessagingEdge(classNodeId, topicId, EdgeKind.SENDS_TO, className + " sends to " + topicName, Map.of("topic", topicName), edges); - if (isConsumer) addEdge(classNodeId, topicId, EdgeKind.RECEIVES_FROM, + if (isConsumer) addMessagingEdge(classNodeId, topicId, EdgeKind.RECEIVES_FROM, className + " receives from " + topicName, Map.of("topic", topicName), edges); } } @@ -178,14 +172,4 @@ private String ensureTopicNode(String name, Set seen, List nod return id; } - private void addEdge(String sourceId, String targetId, EdgeKind kind, String label, - Map props, List edges) { - CodeEdge edge = new CodeEdge(); - edge.setId(sourceId + "->" + kind.getValue() + "->" + targetId); - edge.setKind(kind); - edge.setSourceId(sourceId); - edge.setTarget(new CodeNode(targetId, NodeKind.QUEUE, label)); - edge.setProperties(new LinkedHashMap<>(props)); - edges.add(edge); - } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/kotlin/KotlinStructuresDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/kotlin/KotlinStructuresDetector.java index 18c86c20..fcf2347c 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/kotlin/KotlinStructuresDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/kotlin/KotlinStructuresDetector.java @@ -4,6 +4,7 @@ import io.github.randomcodespace.iq.grammar.AntlrParserFactory; import io.github.randomcodespace.iq.detector.DetectorContext; import io.github.randomcodespace.iq.detector.DetectorResult; +import io.github.randomcodespace.iq.detector.StructuresDetectorHelper; import io.github.randomcodespace.iq.model.CodeEdge; import io.github.randomcodespace.iq.model.CodeNode; import io.github.randomcodespace.iq.model.EdgeKind; @@ -63,11 +64,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { Matcher m = IMPORT_RE.matcher(text); while (m.find()) { - String target = m.group(1); - CodeEdge e = new CodeEdge(); e.setId(fp + ":imports:" + target); - e.setKind(EdgeKind.IMPORTS); e.setSourceId(fp); - e.setTarget(new CodeNode(target, NodeKind.MODULE, target)); - edges.add(e); + StructuresDetectorHelper.addImportEdge(fp, m.group(1), edges); } m = CLASS_RE.matcher(text); @@ -75,18 +72,12 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { String className = m.group(1); String supertypesStr = m.group(2); String nodeId = fp + ":" + className; - CodeNode n = new CodeNode(); n.setId(nodeId); - n.setKind(NodeKind.CLASS); n.setLabel(className); n.setFqn(className); - n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); - nodes.add(n); + nodes.add(StructuresDetectorHelper.createStructureNode(fp, className, NodeKind.CLASS, findLineNumber(text, m.start()))); if (supertypesStr != null) { for (String st : supertypesStr.split(",")) { st = st.trim().split("\\(")[0].split("<")[0].trim(); if (!st.isEmpty()) { - CodeEdge e = new CodeEdge(); e.setId(nodeId + ":extends:" + st); - e.setKind(EdgeKind.EXTENDS); e.setSourceId(nodeId); - e.setTarget(new CodeNode(st, NodeKind.CLASS, st)); - edges.add(e); + StructuresDetectorHelper.addExtendsEdge(nodeId, st, NodeKind.CLASS, edges); } } } @@ -94,30 +85,19 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { m = INTERFACE_RE.matcher(text); while (m.find()) { - String name = m.group(1); - CodeNode n = new CodeNode(); n.setId(fp + ":" + name); - n.setKind(NodeKind.INTERFACE); n.setLabel(name); n.setFqn(name); - n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); - nodes.add(n); + nodes.add(StructuresDetectorHelper.createStructureNode(fp, m.group(1), NodeKind.INTERFACE, findLineNumber(text, m.start()))); } m = OBJECT_RE.matcher(text); while (m.find()) { - String name = m.group(1); - CodeNode n = new CodeNode(); n.setId(fp + ":" + name); - n.setKind(NodeKind.CLASS); n.setLabel(name); n.setFqn(name); - n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); + CodeNode n = StructuresDetectorHelper.createStructureNode(fp, m.group(1), NodeKind.CLASS, findLineNumber(text, m.start())); n.getProperties().put("type", "object"); nodes.add(n); } m = FUN_RE.matcher(text); while (m.find()) { - String name = m.group(1); - CodeNode n = new CodeNode(); n.setId(fp + ":" + name); - n.setKind(NodeKind.METHOD); n.setLabel(name); n.setFqn(name); - n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); - nodes.add(n); + nodes.add(StructuresDetectorHelper.createStructureNode(fp, m.group(1), NodeKind.METHOD, findLineNumber(text, m.start()))); } return DetectorResult.of(nodes, edges); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/CeleryTaskDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/CeleryTaskDetector.java index f6e7aca0..e3775ee2 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/CeleryTaskDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/CeleryTaskDetector.java @@ -100,40 +100,17 @@ public void enterDecorated(Python3Parser.DecoratedContext decorated) { int line = lineOf(dec); String queueId = "queue:" + (moduleName != null ? moduleName : "") + ":celery:" + taskName; - CodeNode queueNode = new CodeNode(); - queueNode.setId(queueId); - queueNode.setKind(NodeKind.QUEUE); - queueNode.setLabel("celery:" + taskName); - queueNode.setModule(moduleName); - queueNode.setFilePath(filePath); - queueNode.setLineStart(line); - queueNode.getProperties().put("broker", "celery"); - queueNode.getProperties().put("task_name", taskName); - queueNode.getProperties().put("function", funcName); - nodes.add(queueNode); + nodes.add(createQueueNode(queueId, taskName, funcName, moduleName, filePath, line)); String methodId = "method:" + filePath + "::" + funcName; - CodeNode methodNode = new CodeNode(); - methodNode.setId(methodId); - methodNode.setKind(NodeKind.METHOD); - methodNode.setLabel(funcName); - methodNode.setFqn(filePath + "::" + funcName); - methodNode.setModule(moduleName); - methodNode.setFilePath(filePath); - methodNode.setLineStart(line); - nodes.add(methodNode); - - CodeEdge consumesEdge = new CodeEdge(); - consumesEdge.setId(methodId + "->consumes->" + queueId); - consumesEdge.setKind(EdgeKind.CONSUMES); - consumesEdge.setSourceId(methodId); - edges.add(consumesEdge); + nodes.add(createMethodNode(methodId, funcName, moduleName, filePath, line)); + + edges.add(createConsumesEdge(methodId, queueId)); } } @Override public void enterAtom_expr(Python3Parser.Atom_exprContext atomExpr) { - // Detect task.delay() / task.apply_async() calls String exprText = atomExpr.getText(); Matcher callMatcher = TASK_CALL.matcher(exprText); if (callMatcher.find()) { @@ -143,11 +120,7 @@ public void enterAtom_expr(Python3Parser.Atom_exprContext atomExpr) { String queueId = "queue:" + (moduleName != null ? moduleName : "") + ":celery:" + taskRef; String callerId = "method:" + filePath + "::caller_l" + line; - CodeEdge producesEdge = new CodeEdge(); - producesEdge.setId(callerId + "->produces->" + queueId); - producesEdge.setKind(EdgeKind.PRODUCES); - producesEdge.setSourceId(callerId); - edges.add(producesEdge); + edges.add(createProducesEdge(callerId, queueId)); } } }, tree); @@ -173,34 +146,12 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { int line = findLineNumber(text, taskMatcher.start()); String queueId = "queue:" + (moduleName != null ? moduleName : "") + ":celery:" + taskName; - CodeNode queueNode = new CodeNode(); - queueNode.setId(queueId); - queueNode.setKind(NodeKind.QUEUE); - queueNode.setLabel("celery:" + taskName); - queueNode.setModule(moduleName); - queueNode.setFilePath(filePath); - queueNode.setLineStart(line); - queueNode.getProperties().put("broker", "celery"); - queueNode.getProperties().put("task_name", taskName); - queueNode.getProperties().put("function", funcName); - nodes.add(queueNode); + nodes.add(createQueueNode(queueId, taskName, funcName, moduleName, filePath, line)); String methodId = "method:" + filePath + "::" + funcName; - CodeNode methodNode = new CodeNode(); - methodNode.setId(methodId); - methodNode.setKind(NodeKind.METHOD); - methodNode.setLabel(funcName); - methodNode.setFqn(filePath + "::" + funcName); - methodNode.setModule(moduleName); - methodNode.setFilePath(filePath); - methodNode.setLineStart(line); - nodes.add(methodNode); - - CodeEdge consumesEdge = new CodeEdge(); - consumesEdge.setId(methodId + "->consumes->" + queueId); - consumesEdge.setKind(EdgeKind.CONSUMES); - consumesEdge.setSourceId(methodId); - edges.add(consumesEdge); + nodes.add(createMethodNode(methodId, funcName, moduleName, filePath, line)); + + edges.add(createConsumesEdge(methodId, queueId)); } Matcher callMatcher = TASK_CALL.matcher(text); @@ -211,13 +162,53 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { String queueId = "queue:" + (moduleName != null ? moduleName : "") + ":celery:" + taskRef; String callerId = "method:" + filePath + "::caller_l" + line; - CodeEdge producesEdge = new CodeEdge(); - producesEdge.setId(callerId + "->produces->" + queueId); - producesEdge.setKind(EdgeKind.PRODUCES); - producesEdge.setSourceId(callerId); - edges.add(producesEdge); + edges.add(createProducesEdge(callerId, queueId)); } return DetectorResult.of(nodes, edges); } + + private static CodeNode createQueueNode(String queueId, String taskName, String funcName, + String moduleName, String filePath, int line) { + CodeNode node = new CodeNode(); + node.setId(queueId); + node.setKind(NodeKind.QUEUE); + node.setLabel("celery:" + taskName); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.getProperties().put("broker", "celery"); + node.getProperties().put("task_name", taskName); + node.getProperties().put("function", funcName); + return node; + } + + private static CodeNode createMethodNode(String methodId, String funcName, + String moduleName, String filePath, int line) { + CodeNode node = new CodeNode(); + node.setId(methodId); + node.setKind(NodeKind.METHOD); + node.setLabel(funcName); + node.setFqn(filePath + "::" + funcName); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + return node; + } + + private static CodeEdge createConsumesEdge(String methodId, String queueId) { + CodeEdge edge = new CodeEdge(); + edge.setId(methodId + "->consumes->" + queueId); + edge.setKind(EdgeKind.CONSUMES); + edge.setSourceId(methodId); + return edge; + } + + private static CodeEdge createProducesEdge(String callerId, String queueId) { + CodeEdge edge = new CodeEdge(); + edge.setId(callerId + "->produces->" + queueId); + edge.setKind(EdgeKind.PRODUCES); + edge.setSourceId(callerId); + return edge; + } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoAuthDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoAuthDetector.java index d421ab0b..5ed00883 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoAuthDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoAuthDetector.java @@ -67,60 +67,20 @@ public void enterDecorated(Python3Parser.DecoratedContext decorated) { if (dec.dotted_name() == null) continue; String decoratorName = dec.dotted_name().getText(); - // @login_required if ("login_required".equals(decoratorName)) { - int line = lineOf(dec); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":login_required:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("@login_required"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("@login_required")); - node.getProperties().put("auth_type", "django"); - node.getProperties().put("permissions", List.of()); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createLoginRequiredGuard(filePath, moduleName, lineOf(dec))); } - // @permission_required("perm") if ("permission_required".equals(decoratorName) && dec.arglist() != null) { - int line = lineOf(dec); String permission = extractFirstStringArg(dec.arglist()); if (permission == null) permission = ""; - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":permission_required:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("@permission_required(" + permission + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("@permission_required")); - node.getProperties().put("auth_type", "django"); - node.getProperties().put("permissions", List.of(permission)); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createPermissionRequiredGuard(filePath, moduleName, lineOf(dec), permission)); } - // @user_passes_test(fn) if ("user_passes_test".equals(decoratorName) && dec.arglist() != null) { - int line = lineOf(dec); String testFunc = extractFirstArgName(dec.arglist()); if (testFunc == null) testFunc = ""; - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":user_passes_test:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("@user_passes_test(" + testFunc + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("@user_passes_test")); - node.getProperties().put("auth_type", "django"); - node.getProperties().put("permissions", List.of()); - node.getProperties().put("test_function", testFunc); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createUserPassesTestGuard(filePath, moduleName, lineOf(dec), testFunc)); } } } @@ -134,21 +94,7 @@ public void enterClassdef(Python3Parser.ClassdefContext classCtx) { for (var arg : classCtx.arglist().argument()) { String base = arg.getText().trim(); if (AUTH_MIXINS.containsKey(base)) { - int line = lineOf(classCtx); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":" + base + ":" + line); - node.setKind(NodeKind.GUARD); - node.setLabel(className + "(" + base + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("mixin:" + base)); - node.getProperties().put("auth_type", "django"); - node.getProperties().put("permissions", List.of()); - node.getProperties().put("mixin", base); - node.getProperties().put("class_name", className); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createMixinGuard(filePath, moduleName, lineOf(classCtx), className, base)); } } } @@ -169,56 +115,17 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { Matcher m = LOGIN_REQUIRED_RE.matcher(text); while (m.find()) { - int line = findLineNumber(text, m.start()); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":login_required:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("@login_required"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("@login_required")); - node.getProperties().put("auth_type", "django"); - node.getProperties().put("permissions", List.of()); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createLoginRequiredGuard(filePath, moduleName, findLineNumber(text, m.start()))); } m = PERMISSION_REQUIRED_RE.matcher(text); while (m.find()) { - int line = findLineNumber(text, m.start()); - String permission = m.group(1); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":permission_required:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("@permission_required(" + permission + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("@permission_required")); - node.getProperties().put("auth_type", "django"); - node.getProperties().put("permissions", List.of(permission)); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createPermissionRequiredGuard(filePath, moduleName, findLineNumber(text, m.start()), m.group(1))); } m = USER_PASSES_TEST_RE.matcher(text); while (m.find()) { - int line = findLineNumber(text, m.start()); - String testFunc = m.group(1); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":user_passes_test:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("@user_passes_test(" + testFunc + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("@user_passes_test")); - node.getProperties().put("auth_type", "django"); - node.getProperties().put("permissions", List.of()); - node.getProperties().put("test_function", testFunc); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createUserPassesTestGuard(filePath, moduleName, findLineNumber(text, m.start()), m.group(1))); } m = MIXIN_RE.matcher(text); @@ -229,21 +136,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { for (String base : bases) { String trimmed = base.trim(); if (AUTH_MIXINS.containsKey(trimmed)) { - int line = findLineNumber(text, m.start()); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":" + trimmed + ":" + line); - node.setKind(NodeKind.GUARD); - node.setLabel(className + "(" + trimmed + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("mixin:" + trimmed)); - node.getProperties().put("auth_type", "django"); - node.getProperties().put("permissions", List.of()); - node.getProperties().put("mixin", trimmed); - node.getProperties().put("class_name", className); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createMixinGuard(filePath, moduleName, findLineNumber(text, m.start()), className, trimmed)); } } } @@ -251,6 +144,69 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { return DetectorResult.of(nodes, List.of()); } + private static CodeNode createLoginRequiredGuard(String filePath, String moduleName, int line) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":login_required:" + line); + node.setKind(NodeKind.GUARD); + node.setLabel("@login_required"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("@login_required")); + node.getProperties().put("auth_type", "django"); + node.getProperties().put("permissions", List.of()); + node.getProperties().put("auth_required", true); + return node; + } + + private static CodeNode createPermissionRequiredGuard(String filePath, String moduleName, int line, String permission) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":permission_required:" + line); + node.setKind(NodeKind.GUARD); + node.setLabel("@permission_required(" + permission + ")"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("@permission_required")); + node.getProperties().put("auth_type", "django"); + node.getProperties().put("permissions", List.of(permission)); + node.getProperties().put("auth_required", true); + return node; + } + + private static CodeNode createUserPassesTestGuard(String filePath, String moduleName, int line, String testFunc) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":user_passes_test:" + line); + node.setKind(NodeKind.GUARD); + node.setLabel("@user_passes_test(" + testFunc + ")"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("@user_passes_test")); + node.getProperties().put("auth_type", "django"); + node.getProperties().put("permissions", List.of()); + node.getProperties().put("test_function", testFunc); + node.getProperties().put("auth_required", true); + return node; + } + + private static CodeNode createMixinGuard(String filePath, String moduleName, int line, String className, String mixin) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":" + mixin + ":" + line); + node.setKind(NodeKind.GUARD); + node.setLabel(className + "(" + mixin + ")"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("mixin:" + mixin)); + node.getProperties().put("auth_type", "django"); + node.getProperties().put("permissions", List.of()); + node.getProperties().put("mixin", mixin); + node.getProperties().put("class_name", className); + node.getProperties().put("auth_required", true); + return node; + } + private static String extractFirstStringArg(Python3Parser.ArglistContext arglist) { if (arglist == null) return null; for (var arg : arglist.argument()) { diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoModelDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoModelDetector.java index e6480d55..fce9b263 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoModelDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoModelDetector.java @@ -89,18 +89,7 @@ public void enterClassdef(Python3Parser.ClassdefContext classCtx) { int line = lineOf(classCtx); String nodeId = "django:" + filePath + ":manager:" + className; managerNames.put(className, nodeId); - - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.REPOSITORY); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "django"); - node.getProperties().put("type", "manager"); - nodes.add(node); + nodes.add(createManagerNode(nodeId, className, filePath, moduleName, line)); } } }, tree); @@ -117,96 +106,18 @@ public void enterClassdef(Python3Parser.ClassdefContext classCtx) { if (bases == null || !bases.matches(".*\\bModel\\b.*")) return; int line = lineOf(classCtx); - // Get class body text for field/meta extraction String classBody = extractClassBody(text, classCtx); - // Extract fields - Map fields = new LinkedHashMap<>(); - Matcher fieldMatcher = FIELD_RE.matcher(classBody); - while (fieldMatcher.find()) { - fields.put(fieldMatcher.group(1), fieldMatcher.group(2)); - } - - // Extract Meta properties - String tableName = null; - String ordering = null; - Matcher metaMatch = META_CLASS_RE.matcher(classBody); - if (metaMatch.find()) { - int metaStart = metaMatch.end(); - int metaEnd = classBody.length(); - Matcher metaEndMatcher = META_END_RE.matcher(classBody.substring(metaStart)); - if (metaEndMatcher.find()) { - metaEnd = metaStart + metaEndMatcher.start(); - } - String metaBlock = classBody.substring(metaStart, metaEnd); - Matcher tableMatch = META_TABLE_RE.matcher(metaBlock); - if (tableMatch.find()) { - tableName = tableMatch.group(1); - } - Matcher orderingMatch = META_ORDERING_RE.matcher(metaBlock); - if (orderingMatch.find()) { - ordering = orderingMatch.group(1); - } - } + Map fields = extractFields(classBody); + String[] meta = extractMeta(classBody); String nodeId = "django:" + filePath + ":model:" + className; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENTITY); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("fields", fields); - node.getProperties().put("framework", "django"); - if (tableName != null) { - node.getProperties().put("table_name", tableName); - } - if (ordering != null) { - node.getProperties().put("ordering", ordering); - } - nodes.add(node); + nodes.add(createModelNode(nodeId, className, filePath, moduleName, line, fields, meta[0], meta[1])); addDbEdge(nodeId, ctx.registry(), nodes, edges); - // FK / OneToOne edges - Matcher fkMatcher = FK_RE.matcher(classBody); - while (fkMatcher.find()) { - String targetClassName = fkMatcher.group(2); - String targetId = "django:" + filePath + ":model:" + targetClassName; - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->depends_on->" + targetId); - edge.setKind(EdgeKind.DEPENDS_ON); - edge.setSourceId(nodeId); - edge.setTarget(new CodeNode("*:" + targetClassName, NodeKind.ENTITY, targetClassName)); - edges.add(edge); - } - - // M2M edges - Matcher m2mMatcher = M2M_RE.matcher(classBody); - while (m2mMatcher.find()) { - String targetClassName = m2mMatcher.group(2); - String targetId = "django:" + filePath + ":model:" + targetClassName; - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->depends_on->" + targetId); - edge.setKind(EdgeKind.DEPENDS_ON); - edge.setSourceId(nodeId); - edge.setTarget(new CodeNode("*:" + targetClassName, NodeKind.ENTITY, targetClassName)); - edges.add(edge); - } - - // Manager assignments - Matcher maMatcher = MANAGER_ASSIGNMENT_RE.matcher(classBody); - while (maMatcher.find()) { - String mgrClass = maMatcher.group(2); - if (managerNames.containsKey(mgrClass)) { - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->queries->" + managerNames.get(mgrClass)); - edge.setKind(EdgeKind.QUERIES); - edge.setSourceId(nodeId); - edges.add(edge); - } - } + addFkEdges(classBody, nodeId, filePath, edges); + addM2mEdges(classBody, nodeId, filePath, edges); + addManagerAssignmentEdges(classBody, nodeId, managerNames, edges); } }, tree); @@ -232,18 +143,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { int line = findLineNumber(text, mgrMatcher.start()); String nodeId = "django:" + filePath + ":manager:" + mgrName; managerNames.put(mgrName, nodeId); - - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.REPOSITORY); - node.setLabel(mgrName); - node.setFqn(filePath + "::" + mgrName); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("framework", "django"); - node.getProperties().put("type", "manager"); - nodes.add(node); + nodes.add(createManagerNode(nodeId, mgrName, filePath, moduleName, line)); } // Detect models @@ -261,91 +161,135 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { classBody = text.substring(classStart); } - Map fields = new LinkedHashMap<>(); - Matcher fieldMatcher = FIELD_RE.matcher(classBody); - while (fieldMatcher.find()) { - fields.put(fieldMatcher.group(1), fieldMatcher.group(2)); - } - - String tableName = null; - String ordering = null; - Matcher metaMatch = META_CLASS_RE.matcher(classBody); - if (metaMatch.find()) { - int metaStart = metaMatch.end(); - int metaEnd = classBody.length(); - Matcher metaEndMatcher = META_END_RE.matcher(classBody.substring(metaStart)); - if (metaEndMatcher.find()) { - metaEnd = metaStart + metaEndMatcher.start(); - } - String metaBlock = classBody.substring(metaStart, metaEnd); - Matcher tableMatch = META_TABLE_RE.matcher(metaBlock); - if (tableMatch.find()) { - tableName = tableMatch.group(1); - } - Matcher orderingMatch = META_ORDERING_RE.matcher(metaBlock); - if (orderingMatch.find()) { - ordering = orderingMatch.group(1); - } - } + Map fields = extractFields(classBody); + String[] meta = extractMeta(classBody); String nodeId = "django:" + filePath + ":model:" + className; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENTITY); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("fields", fields); - node.getProperties().put("framework", "django"); - if (tableName != null) { - node.getProperties().put("table_name", tableName); - } - if (ordering != null) { - node.getProperties().put("ordering", ordering); - } - nodes.add(node); + nodes.add(createModelNode(nodeId, className, filePath, moduleName, line, fields, meta[0], meta[1])); addDbEdge(nodeId, ctx.registry(), nodes, edges); - Matcher fkMatcher = FK_RE.matcher(classBody); - while (fkMatcher.find()) { - String targetClassName = fkMatcher.group(2); - String targetId = "django:" + filePath + ":model:" + targetClassName; - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->depends_on->" + targetId); - edge.setKind(EdgeKind.DEPENDS_ON); - edge.setSourceId(nodeId); - edge.setTarget(new CodeNode("*:" + targetClassName, NodeKind.ENTITY, targetClassName)); - edges.add(edge); + addFkEdges(classBody, nodeId, filePath, edges); + addM2mEdges(classBody, nodeId, filePath, edges); + addManagerAssignmentEdges(classBody, nodeId, managerNames, edges); + } + + return DetectorResult.of(nodes, edges); + } + + // --- Shared helpers --- + + private static CodeNode createManagerNode(String nodeId, String name, String filePath, + String moduleName, int line) { + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.REPOSITORY); + node.setLabel(name); + node.setFqn(filePath + "::" + name); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.getProperties().put("framework", "django"); + node.getProperties().put("type", "manager"); + return node; + } + + private static CodeNode createModelNode(String nodeId, String className, String filePath, + String moduleName, int line, Map fields, String tableName, String ordering) { + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.ENTITY); + node.setLabel(className); + node.setFqn(filePath + "::" + className); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.getProperties().put("fields", fields); + node.getProperties().put("framework", "django"); + if (tableName != null) { + node.getProperties().put("table_name", tableName); + } + if (ordering != null) { + node.getProperties().put("ordering", ordering); + } + return node; + } + + private static Map extractFields(String classBody) { + Map fields = new LinkedHashMap<>(); + Matcher fieldMatcher = FIELD_RE.matcher(classBody); + while (fieldMatcher.find()) { + fields.put(fieldMatcher.group(1), fieldMatcher.group(2)); + } + return fields; + } + + /** + * Extract Meta class properties (table_name and ordering) from a class body. + * @return a two-element array: [tableName, ordering] (either may be null) + */ + private static String[] extractMeta(String classBody) { + String tableName = null; + String ordering = null; + Matcher metaMatch = META_CLASS_RE.matcher(classBody); + if (metaMatch.find()) { + int metaStart = metaMatch.end(); + int metaEnd = classBody.length(); + Matcher metaEndMatcher = META_END_RE.matcher(classBody.substring(metaStart)); + if (metaEndMatcher.find()) { + metaEnd = metaStart + metaEndMatcher.start(); + } + String metaBlock = classBody.substring(metaStart, metaEnd); + Matcher tableMatch = META_TABLE_RE.matcher(metaBlock); + if (tableMatch.find()) { + tableName = tableMatch.group(1); } + Matcher orderingMatch = META_ORDERING_RE.matcher(metaBlock); + if (orderingMatch.find()) { + ordering = orderingMatch.group(1); + } + } + return new String[]{tableName, ordering}; + } - Matcher m2mMatcher = M2M_RE.matcher(classBody); - while (m2mMatcher.find()) { - String targetClassName = m2mMatcher.group(2); - String targetId = "django:" + filePath + ":model:" + targetClassName; + private static void addFkEdges(String classBody, String nodeId, String filePath, List edges) { + Matcher fkMatcher = FK_RE.matcher(classBody); + while (fkMatcher.find()) { + String targetClassName = fkMatcher.group(2); + edges.add(createDependsOnEdge(nodeId, filePath, targetClassName)); + } + } + + private static void addM2mEdges(String classBody, String nodeId, String filePath, List edges) { + Matcher m2mMatcher = M2M_RE.matcher(classBody); + while (m2mMatcher.find()) { + String targetClassName = m2mMatcher.group(2); + edges.add(createDependsOnEdge(nodeId, filePath, targetClassName)); + } + } + + private static CodeEdge createDependsOnEdge(String nodeId, String filePath, String targetClassName) { + String targetId = "django:" + filePath + ":model:" + targetClassName; + CodeEdge edge = new CodeEdge(); + edge.setId(nodeId + "->depends_on->" + targetId); + edge.setKind(EdgeKind.DEPENDS_ON); + edge.setSourceId(nodeId); + edge.setTarget(new CodeNode("*:" + targetClassName, NodeKind.ENTITY, targetClassName)); + return edge; + } + + private static void addManagerAssignmentEdges(String classBody, String nodeId, + Map managerNames, List edges) { + Matcher maMatcher = MANAGER_ASSIGNMENT_RE.matcher(classBody); + while (maMatcher.find()) { + String mgrClass = maMatcher.group(2); + if (managerNames.containsKey(mgrClass)) { CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->depends_on->" + targetId); - edge.setKind(EdgeKind.DEPENDS_ON); + edge.setId(nodeId + "->queries->" + managerNames.get(mgrClass)); + edge.setKind(EdgeKind.QUERIES); edge.setSourceId(nodeId); - edge.setTarget(new CodeNode("*:" + targetClassName, NodeKind.ENTITY, targetClassName)); edges.add(edge); } - - Matcher maMatcher = MANAGER_ASSIGNMENT_RE.matcher(classBody); - while (maMatcher.find()) { - String mgrClass = maMatcher.group(2); - if (managerNames.containsKey(mgrClass)) { - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->queries->" + managerNames.get(mgrClass)); - edge.setKind(EdgeKind.QUERIES); - edge.setSourceId(nodeId); - edges.add(edge); - } - } } - - return DetectorResult.of(nodes, edges); } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoViewDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoViewDetector.java index b706ea10..367769f6 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoViewDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/DjangoViewDetector.java @@ -52,24 +52,8 @@ protected DetectorResult detectWithAst(ParseTree tree, DetectorContext ctx) { if (text.contains("urlpatterns")) { Matcher urlMatcher = URL_PATTERN.matcher(text); while (urlMatcher.find()) { - String pathPattern = urlMatcher.group(1); - String viewRef = urlMatcher.group(2); - int line = findLineNumber(text, urlMatcher.start()); - - String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":ALL:" + pathPattern; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENDPOINT); - node.setLabel(pathPattern); - node.setFqn(viewRef); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("protocol", "REST"); - node.getProperties().put("path_pattern", pathPattern); - node.getProperties().put("framework", "django"); - node.getProperties().put("view_reference", viewRef); - nodes.add(node); + nodes.add(createUrlPatternEndpoint(filePath, moduleName, + findLineNumber(text, urlMatcher.start()), urlMatcher.group(1), urlMatcher.group(2))); } } @@ -80,26 +64,12 @@ public void enterClassdef(Python3Parser.ClassdefContext classCtx) { if (classCtx.name() == null) return; String className = classCtx.name().getText(); - // Check if any base class contains View, ViewSet, or Mixin String bases = getBaseClassesText(classCtx); if (bases == null || (!bases.contains("View") && !bases.contains("ViewSet") && !bases.contains("Mixin"))) { return; } - int line = lineOf(classCtx); - String nodeId = "class:" + filePath + "::" + className; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.CLASS); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("extends:" + bases.trim())); - node.getProperties().put("framework", "django"); - node.getProperties().put("stereotype", "view"); - nodes.add(node); + nodes.add(createCbvNode(filePath, moduleName, lineOf(classCtx), className, bases.trim())); } }, tree); @@ -119,49 +89,53 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { if (text.contains("urlpatterns")) { Matcher urlMatcher = URL_PATTERN.matcher(text); while (urlMatcher.find()) { - String pathPattern = urlMatcher.group(1); - String viewRef = urlMatcher.group(2); - int line = findLineNumber(text, urlMatcher.start()); - - String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":ALL:" + pathPattern; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENDPOINT); - node.setLabel(pathPattern); - node.setFqn(viewRef); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("protocol", "REST"); - node.getProperties().put("path_pattern", pathPattern); - node.getProperties().put("framework", "django"); - node.getProperties().put("view_reference", viewRef); - nodes.add(node); + nodes.add(createUrlPatternEndpoint(filePath, moduleName, + findLineNumber(text, urlMatcher.start()), urlMatcher.group(1), urlMatcher.group(2))); } } Matcher cbvMatcher = CBV_PATTERN.matcher(text); while (cbvMatcher.find()) { - String className = cbvMatcher.group(1); - String bases = cbvMatcher.group(2); - int line = findLineNumber(text, cbvMatcher.start()); - - String nodeId = "class:" + filePath + "::" + className; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.CLASS); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("extends:" + bases.trim())); - node.getProperties().put("framework", "django"); - node.getProperties().put("stereotype", "view"); - nodes.add(node); + nodes.add(createCbvNode(filePath, moduleName, + findLineNumber(text, cbvMatcher.start()), cbvMatcher.group(1), cbvMatcher.group(2).trim())); } return DetectorResult.of(nodes, List.of()); } + private static CodeNode createUrlPatternEndpoint(String filePath, String moduleName, int line, + String pathPattern, String viewRef) { + String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":ALL:" + pathPattern; + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.ENDPOINT); + node.setLabel(pathPattern); + node.setFqn(viewRef); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.getProperties().put("protocol", "REST"); + node.getProperties().put("path_pattern", pathPattern); + node.getProperties().put("framework", "django"); + node.getProperties().put("view_reference", viewRef); + return node; + } + + private static CodeNode createCbvNode(String filePath, String moduleName, int line, + String className, String bases) { + String nodeId = "class:" + filePath + "::" + className; + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.CLASS); + node.setLabel(className); + node.setFqn(filePath + "::" + className); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("extends:" + bases)); + node.getProperties().put("framework", "django"); + node.getProperties().put("stereotype", "view"); + return node; + } + } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/FastAPIAuthDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/FastAPIAuthDetector.java index 7585a249..d51fb3e8 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/FastAPIAuthDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/FastAPIAuthDetector.java @@ -62,104 +62,33 @@ protected DetectorResult detectWithAst(ParseTree tree, DetectorContext ctx) { public void enterAtom_expr(Python3Parser.Atom_exprContext atomExpr) { String text = atomExpr.getText(); - // Depends(get_current_user...) / Depends(require_auth...) / Depends(auth...) if (text.startsWith("Depends(")) { Matcher m = DEPENDS_AUTH_RE.matcher(text); if (m.find()) { - int line = lineOf(atomExpr); - String depName = m.group(1); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":Depends:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("Depends(" + depName + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("Depends(" + depName + ")")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "oauth2"); - node.getProperties().put("dependency", depName); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createDependsGuard(filePath, moduleName, lineOf(atomExpr), m.group(1))); } } - // Security(scheme) if (text.startsWith("Security(")) { Matcher m = SECURITY_RE.matcher(text); if (m.find()) { - int line = lineOf(atomExpr); - String schemeName = m.group(1); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":Security:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("Security(" + schemeName + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("Security(" + schemeName + ")")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "oauth2"); - node.getProperties().put("scheme", schemeName); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createSecurityGuard(filePath, moduleName, lineOf(atomExpr), m.group(1))); } } - // HTTPBearer() if (text.contains("HTTPBearer(")) { - int line = lineOf(atomExpr); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":HTTPBearer:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("HTTPBearer()"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("HTTPBearer")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "bearer"); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createHttpBearerGuard(filePath, moduleName, lineOf(atomExpr))); } - // OAuth2PasswordBearer(tokenUrl=...) if (text.contains("OAuth2PasswordBearer(")) { Matcher m = OAUTH2_PASSWORD_BEARER_RE.matcher(text); if (m.find()) { - int line = lineOf(atomExpr); - String tokenUrl = m.group(1); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":OAuth2PasswordBearer:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("OAuth2PasswordBearer(" + tokenUrl + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("OAuth2PasswordBearer")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "oauth2"); - node.getProperties().put("token_url", tokenUrl); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createOAuth2PasswordBearerGuard(filePath, moduleName, lineOf(atomExpr), m.group(1))); } } - // HTTPBasic() if (text.contains("HTTPBasic(")) { - int line = lineOf(atomExpr); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":HTTPBasic:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("HTTPBasic()"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("HTTPBasic")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "basic"); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createHttpBasicGuard(filePath, moduleName, lineOf(atomExpr))); } } }, tree); @@ -179,95 +108,107 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { Matcher m = DEPENDS_AUTH_RE.matcher(text); while (m.find()) { - int line = findLineNumber(text, m.start()); - String depName = m.group(1); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":Depends:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("Depends(" + depName + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("Depends(" + depName + ")")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "oauth2"); - node.getProperties().put("dependency", depName); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createDependsGuard(filePath, moduleName, findLineNumber(text, m.start()), m.group(1))); } m = SECURITY_RE.matcher(text); while (m.find()) { - int line = findLineNumber(text, m.start()); - String schemeName = m.group(1); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":Security:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("Security(" + schemeName + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("Security(" + schemeName + ")")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "oauth2"); - node.getProperties().put("scheme", schemeName); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createSecurityGuard(filePath, moduleName, findLineNumber(text, m.start()), m.group(1))); } m = HTTP_BEARER_RE.matcher(text); while (m.find()) { - int line = findLineNumber(text, m.start()); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":HTTPBearer:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("HTTPBearer()"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("HTTPBearer")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "bearer"); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createHttpBearerGuard(filePath, moduleName, findLineNumber(text, m.start()))); } m = OAUTH2_PASSWORD_BEARER_RE.matcher(text); while (m.find()) { - int line = findLineNumber(text, m.start()); - String tokenUrl = m.group(1); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":OAuth2PasswordBearer:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("OAuth2PasswordBearer(" + tokenUrl + ")"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("OAuth2PasswordBearer")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "oauth2"); - node.getProperties().put("token_url", tokenUrl); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createOAuth2PasswordBearerGuard(filePath, moduleName, findLineNumber(text, m.start()), m.group(1))); } m = HTTP_BASIC_RE.matcher(text); while (m.find()) { - int line = findLineNumber(text, m.start()); - CodeNode node = new CodeNode(); - node.setId("auth:" + filePath + ":HTTPBasic:" + line); - node.setKind(NodeKind.GUARD); - node.setLabel("HTTPBasic()"); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(List.of("HTTPBasic")); - node.getProperties().put("auth_type", "fastapi"); - node.getProperties().put("auth_flow", "basic"); - node.getProperties().put("auth_required", true); - nodes.add(node); + nodes.add(createHttpBasicGuard(filePath, moduleName, findLineNumber(text, m.start()))); } return DetectorResult.of(nodes, List.of()); } + + private static CodeNode createDependsGuard(String filePath, String moduleName, int line, String depName) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":Depends:" + line); + node.setKind(NodeKind.GUARD); + node.setLabel("Depends(" + depName + ")"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("Depends(" + depName + ")")); + node.getProperties().put("auth_type", "fastapi"); + node.getProperties().put("auth_flow", "oauth2"); + node.getProperties().put("dependency", depName); + node.getProperties().put("auth_required", true); + return node; + } + + private static CodeNode createSecurityGuard(String filePath, String moduleName, int line, String schemeName) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":Security:" + line); + node.setKind(NodeKind.GUARD); + node.setLabel("Security(" + schemeName + ")"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("Security(" + schemeName + ")")); + node.getProperties().put("auth_type", "fastapi"); + node.getProperties().put("auth_flow", "oauth2"); + node.getProperties().put("scheme", schemeName); + node.getProperties().put("auth_required", true); + return node; + } + + private static CodeNode createHttpBearerGuard(String filePath, String moduleName, int line) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":HTTPBearer:" + line); + node.setKind(NodeKind.GUARD); + node.setLabel("HTTPBearer()"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("HTTPBearer")); + node.getProperties().put("auth_type", "fastapi"); + node.getProperties().put("auth_flow", "bearer"); + node.getProperties().put("auth_required", true); + return node; + } + + private static CodeNode createOAuth2PasswordBearerGuard(String filePath, String moduleName, int line, String tokenUrl) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":OAuth2PasswordBearer:" + line); + node.setKind(NodeKind.GUARD); + node.setLabel("OAuth2PasswordBearer(" + tokenUrl + ")"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("OAuth2PasswordBearer")); + node.getProperties().put("auth_type", "fastapi"); + node.getProperties().put("auth_flow", "oauth2"); + node.getProperties().put("token_url", tokenUrl); + node.getProperties().put("auth_required", true); + return node; + } + + private static CodeNode createHttpBasicGuard(String filePath, String moduleName, int line) { + CodeNode node = new CodeNode(); + node.setId("auth:" + filePath + ":HTTPBasic:" + line); + node.setKind(NodeKind.GUARD); + node.setLabel("HTTPBasic()"); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(List.of("HTTPBasic")); + node.getProperties().put("auth_type", "fastapi"); + node.getProperties().put("auth_flow", "basic"); + node.getProperties().put("auth_required", true); + return node; + } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/FastAPIRouteDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/FastAPIRouteDetector.java index b47c2702..ead097c5 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/FastAPIRouteDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/FastAPIRouteDetector.java @@ -101,21 +101,7 @@ public void enterDecorated(Python3Parser.DecoratedContext decorated) { String method = methodName.toUpperCase(); int line = lineOf(dec); - String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":" + method + ":" + fullPath; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENDPOINT); - node.setLabel(method + " " + fullPath); - node.setFqn(filePath + "::" + funcName); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("protocol", "REST"); - node.getProperties().put("http_method", method); - node.getProperties().put("path_pattern", fullPath); - node.getProperties().put("framework", "fastapi"); - node.getProperties().put("router", routerName); - nodes.add(node); + nodes.add(createRouteEndpoint(filePath, moduleName, line, method, fullPath, funcName, routerName)); } } }, tree); @@ -151,26 +137,31 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { int line = findLineNumber(text, routeMatcher.start()); - String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":" + method + ":" + fullPath; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENDPOINT); - node.setLabel(method + " " + fullPath); - node.setFqn(filePath + "::" + funcName); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("protocol", "REST"); - node.getProperties().put("http_method", method); - node.getProperties().put("path_pattern", fullPath); - node.getProperties().put("framework", "fastapi"); - node.getProperties().put("router", routerName); - nodes.add(node); + nodes.add(createRouteEndpoint(filePath, moduleName, line, method, fullPath, funcName, routerName)); } return DetectorResult.of(nodes, List.of()); } + private static CodeNode createRouteEndpoint(String filePath, String moduleName, int line, + String method, String fullPath, String funcName, String routerName) { + String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":" + method + ":" + fullPath; + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.ENDPOINT); + node.setLabel(method + " " + fullPath); + node.setFqn(filePath + "::" + funcName); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.getProperties().put("protocol", "REST"); + node.getProperties().put("http_method", method); + node.getProperties().put("path_pattern", fullPath); + node.getProperties().put("framework", "fastapi"); + node.getProperties().put("router", routerName); + return node; + } + /** * Extract the first string literal argument from an arglist. */ diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/FlaskRouteDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/FlaskRouteDetector.java index d3ac12b7..11046ac2 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/FlaskRouteDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/FlaskRouteDetector.java @@ -89,28 +89,9 @@ public void enterDecorated(Python3Parser.DecoratedContext decorated) { int line = lineOf(dec); for (String method : methods) { - String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":" + method + ":" + path; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENDPOINT); - node.setLabel(method + " " + path); - node.setFqn(filePath + "::" + funcName); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("protocol", "REST"); - node.getProperties().put("http_method", method); - node.getProperties().put("path_pattern", path); - node.getProperties().put("framework", "flask"); - node.getProperties().put("blueprint", blueprint); + CodeNode node = createFlaskRouteEndpoint(filePath, moduleName, line, method, path, funcName, blueprint); nodes.add(node); - - String classId = "class:" + filePath + "::" + blueprint; - CodeEdge edge = new CodeEdge(); - edge.setId(classId + "->exposes->" + nodeId); - edge.setKind(EdgeKind.EXPOSES); - edge.setSourceId(classId); - edges.add(edge); + edges.add(createExposesEdge(filePath, blueprint, node.getId())); } } } @@ -149,34 +130,43 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { int line = findLineNumber(text, routeMatcher.start()); for (String method : methods) { - String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":" + method + ":" + path; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENDPOINT); - node.setLabel(method + " " + path); - node.setFqn(filePath + "::" + funcName); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("protocol", "REST"); - node.getProperties().put("http_method", method); - node.getProperties().put("path_pattern", path); - node.getProperties().put("framework", "flask"); - node.getProperties().put("blueprint", blueprint); + CodeNode node = createFlaskRouteEndpoint(filePath, moduleName, line, method, path, funcName, blueprint); nodes.add(node); - - String classId = "class:" + filePath + "::" + blueprint; - CodeEdge edge = new CodeEdge(); - edge.setId(classId + "->exposes->" + nodeId); - edge.setKind(EdgeKind.EXPOSES); - edge.setSourceId(classId); - edges.add(edge); + edges.add(createExposesEdge(filePath, blueprint, node.getId())); } } return DetectorResult.of(nodes, edges); } + private static CodeNode createFlaskRouteEndpoint(String filePath, String moduleName, int line, + String method, String path, String funcName, String blueprint) { + String nodeId = "endpoint:" + (moduleName != null ? moduleName : "") + ":" + method + ":" + path; + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.ENDPOINT); + node.setLabel(method + " " + path); + node.setFqn(filePath + "::" + funcName); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.getProperties().put("protocol", "REST"); + node.getProperties().put("http_method", method); + node.getProperties().put("path_pattern", path); + node.getProperties().put("framework", "flask"); + node.getProperties().put("blueprint", blueprint); + return node; + } + + private static CodeEdge createExposesEdge(String filePath, String blueprint, String endpointId) { + String classId = "class:" + filePath + "::" + blueprint; + CodeEdge edge = new CodeEdge(); + edge.setId(classId + "->exposes->" + endpointId); + edge.setKind(EdgeKind.EXPOSES); + edge.setSourceId(classId); + return edge; + } + private static String extractFirstStringArg(Python3Parser.ArglistContext arglist) { if (arglist == null) return null; for (var arg : arglist.argument()) { diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/PydanticModelDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/PydanticModelDetector.java index 7e99f173..912a2d4f 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/PydanticModelDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/PydanticModelDetector.java @@ -77,80 +77,13 @@ public void enterClassdef(Python3Parser.ClassdefContext classCtx) { if (!bases.contains("BaseModel") && !bases.contains("BaseSettings")) return; String baseClass = bases.trim(); - boolean isSettings = baseClass.contains("BaseSettings"); int line = lineOf(classCtx); - - // Extract class body for field/validator/config extraction String classBody = extractClassBody(text, classCtx); - // Extract fields - List fields = new ArrayList<>(); - Map fieldTypes = new LinkedHashMap<>(); - Matcher fieldMatcher = FIELD_RE.matcher(classBody); - while (fieldMatcher.find()) { - String fname = fieldMatcher.group(1); - String ftype = fieldMatcher.group(2).trim(); - if (!fname.equals("class") && !fname.equals("Config") && !fname.equals("model_config")) { - fields.add(fname); - fieldTypes.put(fname, ftype); - } - } - - // Extract validators - List validators = new ArrayList<>(); - Matcher validatorMatcher = VALIDATOR_RE.matcher(classBody); - while (validatorMatcher.find()) { - validators.add(validatorMatcher.group(1)); - } - - // Extract Config class properties - Map configProps = new LinkedHashMap<>(); - Matcher configMatch = CONFIG_CLASS_RE.matcher(classBody); - if (configMatch.find()) { - int configBlockStart = configMatch.end(); - int configBlockEnd = classBody.length(); - Matcher configEndMatcher = CONFIG_END_RE.matcher(classBody.substring(configBlockStart)); - if (configEndMatcher.find()) { - configBlockEnd = configBlockStart + configEndMatcher.start(); - } - String configBlock = classBody.substring(configBlockStart, configBlockEnd); - Matcher attrMatcher = CONFIG_ATTR_RE.matcher(configBlock); - while (attrMatcher.find()) { - configProps.put(attrMatcher.group(1), attrMatcher.group(2).trim()); - } - } - - NodeKind nodeKind = isSettings ? NodeKind.CONFIG_DEFINITION : NodeKind.ENTITY; String nodeId = "pydantic:" + filePath + ":model:" + className; - - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(nodeKind); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(validators); - node.getProperties().put("fields", fields); - node.getProperties().put("field_types", fieldTypes); - node.getProperties().put("framework", "pydantic"); - node.getProperties().put("base_class", baseClass); - if (!configProps.isEmpty()) { - node.getProperties().put("config", configProps); - } - nodes.add(node); - + nodes.add(createPydanticNode(nodeId, className, filePath, moduleName, line, baseClass, classBody)); knownModels.put(className, nodeId); - - // Inheritance edge - if (knownModels.containsKey(baseClass)) { - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->extends->" + knownModels.get(baseClass)); - edge.setKind(EdgeKind.EXTENDS); - edge.setSourceId(nodeId); - edges.add(edge); - } + addExtendsEdge(nodeId, baseClass, knownModels, edges); } }, tree); @@ -176,8 +109,6 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { String baseClass = classMatcher.group(2); int line = findLineNumber(text, classMatcher.start()); - boolean isSettings = baseClass.contains("BaseSettings"); - int classStart = classMatcher.start(); Matcher nextClassMatcher = NEXT_CLASS_RE.matcher(text.substring(classMatcher.end())); String classBody; @@ -187,73 +118,98 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { classBody = text.substring(classStart); } - List fields = new ArrayList<>(); - Map fieldTypes = new LinkedHashMap<>(); - Matcher fieldMatcher = FIELD_RE.matcher(classBody); - while (fieldMatcher.find()) { - String fname = fieldMatcher.group(1); - String ftype = fieldMatcher.group(2).trim(); - if (!fname.equals("class") && !fname.equals("Config") && !fname.equals("model_config")) { - fields.add(fname); - fieldTypes.put(fname, ftype); - } - } - - List validators = new ArrayList<>(); - Matcher validatorMatcher = VALIDATOR_RE.matcher(classBody); - while (validatorMatcher.find()) { - validators.add(validatorMatcher.group(1)); - } + String nodeId = "pydantic:" + filePath + ":model:" + className; + nodes.add(createPydanticNode(nodeId, className, filePath, moduleName, line, baseClass, classBody)); + knownModels.put(className, nodeId); + addExtendsEdge(nodeId, baseClass, knownModels, edges); + } - Map configProps = new LinkedHashMap<>(); - Matcher configMatch = CONFIG_CLASS_RE.matcher(classBody); - if (configMatch.find()) { - int configBlockStart = configMatch.end(); - int configBlockEnd = classBody.length(); - Matcher configEndMatcher = CONFIG_END_RE.matcher(classBody.substring(configBlockStart)); - if (configEndMatcher.find()) { - configBlockEnd = configBlockStart + configEndMatcher.start(); - } - String configBlock = classBody.substring(configBlockStart, configBlockEnd); - Matcher attrMatcher = CONFIG_ATTR_RE.matcher(configBlock); - while (attrMatcher.find()) { - configProps.put(attrMatcher.group(1), attrMatcher.group(2).trim()); - } - } + return DetectorResult.of(nodes, edges); + } - NodeKind nodeKind = isSettings ? NodeKind.CONFIG_DEFINITION : NodeKind.ENTITY; - String nodeId = "pydantic:" + filePath + ":model:" + className; + // --- Shared helpers --- + + private static CodeNode createPydanticNode(String nodeId, String className, String filePath, + String moduleName, int line, String baseClass, String classBody) { + boolean isSettings = baseClass.contains("BaseSettings"); + + List fields = new ArrayList<>(); + Map fieldTypes = extractFieldsAndTypes(classBody, fields); + List validators = extractValidators(classBody); + Map configProps = extractConfigProps(classBody); + + NodeKind nodeKind = isSettings ? NodeKind.CONFIG_DEFINITION : NodeKind.ENTITY; + + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(nodeKind); + node.setLabel(className); + node.setFqn(filePath + "::" + className); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.setAnnotations(validators); + node.getProperties().put("fields", fields); + node.getProperties().put("field_types", fieldTypes); + node.getProperties().put("framework", "pydantic"); + node.getProperties().put("base_class", baseClass); + if (!configProps.isEmpty()) { + node.getProperties().put("config", configProps); + } + return node; + } - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(nodeKind); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.setAnnotations(validators); - node.getProperties().put("fields", fields); - node.getProperties().put("field_types", fieldTypes); - node.getProperties().put("framework", "pydantic"); - node.getProperties().put("base_class", baseClass); - if (!configProps.isEmpty()) { - node.getProperties().put("config", configProps); + private static Map extractFieldsAndTypes(String classBody, List fields) { + Map fieldTypes = new LinkedHashMap<>(); + Matcher fieldMatcher = FIELD_RE.matcher(classBody); + while (fieldMatcher.find()) { + String fname = fieldMatcher.group(1); + String ftype = fieldMatcher.group(2).trim(); + if (!fname.equals("class") && !fname.equals("Config") && !fname.equals("model_config")) { + fields.add(fname); + fieldTypes.put(fname, ftype); } - nodes.add(node); + } + return fieldTypes; + } - knownModels.put(className, nodeId); + private static List extractValidators(String classBody) { + List validators = new ArrayList<>(); + Matcher validatorMatcher = VALIDATOR_RE.matcher(classBody); + while (validatorMatcher.find()) { + validators.add(validatorMatcher.group(1)); + } + return validators; + } - if (knownModels.containsKey(baseClass)) { - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->extends->" + knownModels.get(baseClass)); - edge.setKind(EdgeKind.EXTENDS); - edge.setSourceId(nodeId); - edges.add(edge); + private static Map extractConfigProps(String classBody) { + Map configProps = new LinkedHashMap<>(); + Matcher configMatch = CONFIG_CLASS_RE.matcher(classBody); + if (configMatch.find()) { + int configBlockStart = configMatch.end(); + int configBlockEnd = classBody.length(); + Matcher configEndMatcher = CONFIG_END_RE.matcher(classBody.substring(configBlockStart)); + if (configEndMatcher.find()) { + configBlockEnd = configBlockStart + configEndMatcher.start(); + } + String configBlock = classBody.substring(configBlockStart, configBlockEnd); + Matcher attrMatcher = CONFIG_ATTR_RE.matcher(configBlock); + while (attrMatcher.find()) { + configProps.put(attrMatcher.group(1), attrMatcher.group(2).trim()); } } + return configProps; + } - return DetectorResult.of(nodes, edges); + private static void addExtendsEdge(String nodeId, String baseClass, + Map knownModels, List edges) { + if (knownModels.containsKey(baseClass)) { + CodeEdge edge = new CodeEdge(); + edge.setId(nodeId + "->extends->" + knownModels.get(baseClass)); + edge.setKind(EdgeKind.EXTENDS); + edge.setSourceId(nodeId); + edges.add(edge); + } } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/PythonStructuresDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/PythonStructuresDetector.java index 84b349e6..4a03d135 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/PythonStructuresDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/PythonStructuresDetector.java @@ -66,23 +66,10 @@ protected DetectorResult detectWithAst(ParseTree tree, DetectorContext ctx) { String moduleName = ctx.moduleName(); // Extract __all__ exports via regex (simpler than walking assignment expressions) - Matcher allMatch = ALL_RE.matcher(text); - List allExports = null; - int allMatchStart = -1; - if (allMatch.find()) { - allMatchStart = allMatch.start(); - String raw = allMatch.group(1); - allExports = new ArrayList<>(); - Matcher qm = QUOTED_NAME_RE.matcher(raw); - while (qm.find()) { - allExports.add(qm.group(1)); - } - } + AllExports allInfo = extractAllExports(text); // Collect decorators by looking at the text (for function/class decorator collection) Map> decoratorMap = collectDecorators(text); - List allExportsFinal = allExports; - int allMatchStartFinal = allMatchStart; // Track classes for enclosing-class detection List classNames = new ArrayList<>(); @@ -100,48 +87,12 @@ public void enterClassdef(Python3Parser.ClassdefContext classCtx) { List annotations = findDecoratorsForLine(decoratorMap, line); - Map properties = new HashMap<>(); String basesStr = getBaseClassesText(classCtx); - if (basesStr != null && !basesStr.isBlank()) { - List bases = new ArrayList<>(); - for (String b : basesStr.split(",")) { - String trimmed = b.trim(); - if (!trimmed.isEmpty()) { - bases.add(trimmed); - } - } - properties.put("bases", bases); - } - if (allExportsFinal != null && allExportsFinal.contains(className)) { - properties.put("exported", true); - } + Map properties = buildBaseProperties(basesStr, className, allInfo.exports); String nodeId = "py:" + fp + ":class:" + className; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.CLASS); - node.setLabel(className); - node.setFqn(className); - node.setModule(moduleName); - node.setFilePath(fp); - node.setLineStart(line); - node.setAnnotations(annotations); - node.setProperties(properties); - nodes.add(node); - - // EXTENDS edges - if (basesStr != null && !basesStr.isBlank()) { - for (String b : basesStr.split(",")) { - String base = b.trim(); - if (!base.isEmpty()) { - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->extends->" + base); - edge.setKind(EdgeKind.EXTENDS); - edge.setSourceId(nodeId); - edges.add(edge); - } - } - } + nodes.add(createClassNode(nodeId, className, fp, moduleName, line, annotations, properties)); + addExtendsEdges(nodeId, basesStr, edges); } @Override @@ -162,47 +113,21 @@ public void enterFuncdef(Python3Parser.FuncdefContext funcCtx) { if (isAsync) { properties.put("async", true); } - if (allExportsFinal != null && allExportsFinal.contains(funcName)) { + if (allInfo.exports != null && allInfo.exports.contains(funcName)) { properties.put("exported", true); } if (indent == 0) { // Top-level function String nodeId = "py:" + fp + ":func:" + funcName; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.METHOD); - node.setLabel(funcName); - node.setFqn(funcName); - node.setModule(moduleName); - node.setFilePath(fp); - node.setLineStart(line); - node.setAnnotations(annotations); - node.setProperties(properties); - nodes.add(node); + nodes.add(createFunctionNode(nodeId, funcName, null, fp, moduleName, line, annotations, properties)); } else { String enclosingClass = findEnclosingClass(classNames, classRanges, line); if (enclosingClass != null) { String nodeId = "py:" + fp + ":class:" + enclosingClass + ":method:" + funcName; properties.put("class", enclosingClass); - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.METHOD); - node.setLabel(enclosingClass + "." + funcName); - node.setFqn(enclosingClass + "." + funcName); - node.setModule(moduleName); - node.setFilePath(fp); - node.setLineStart(line); - node.setAnnotations(annotations); - node.setProperties(properties); - nodes.add(node); - - String classNodeId = "py:" + fp + ":class:" + enclosingClass; - CodeEdge edge = new CodeEdge(); - edge.setId(classNodeId + "->defines->" + nodeId); - edge.setKind(EdgeKind.DEFINES); - edge.setSourceId(classNodeId); - edges.add(edge); + nodes.add(createFunctionNode(nodeId, funcName, enclosingClass, fp, moduleName, line, annotations, properties)); + addDefinesEdge("py:" + fp + ":class:" + enclosingClass, nodeId, edges); } } } @@ -213,11 +138,7 @@ public void enterImport_name(Python3Parser.Import_nameContext importCtx) { if (importCtx.dotted_as_names() != null) { for (var dan : importCtx.dotted_as_names().dotted_as_name()) { String importedName = dan.dotted_name().getText(); - CodeEdge edge = new CodeEdge(); - edge.setId(fp + "->imports->" + importedName); - edge.setKind(EdgeKind.IMPORTS); - edge.setSourceId(fp); - edges.add(edge); + addImportEdge(fp, importedName, edges); } } } @@ -225,9 +146,7 @@ public void enterImport_name(Python3Parser.Import_nameContext importCtx) { @Override public void enterImport_from(Python3Parser.Import_fromContext importCtx) { // from os.path import join - // Extract the module name from dotted_name or dots StringBuilder fromModule = new StringBuilder(); - // Count dots if (importCtx.DOT() != null) { for (var dot : importCtx.DOT()) { fromModule.append("."); @@ -242,28 +161,15 @@ public void enterImport_from(Python3Parser.Import_fromContext importCtx) { fromModule.append(importCtx.dotted_name().getText()); } if (fromModule.length() > 0) { - CodeEdge edge = new CodeEdge(); - edge.setId(fp + "->imports->" + fromModule); - edge.setKind(EdgeKind.IMPORTS); - edge.setSourceId(fp); - edges.add(edge); + addImportEdge(fp, fromModule.toString(), edges); } } }, tree); // __all__ module node - if (allExports != null) { - String moduleNodeId = "py:" + fp + ":module"; - CodeNode moduleNode = new CodeNode(); - moduleNode.setId(moduleNodeId); - moduleNode.setKind(NodeKind.MODULE); - moduleNode.setLabel(fp); - moduleNode.setFqn(fp); - moduleNode.setModule(moduleName); - moduleNode.setFilePath(fp); - moduleNode.setLineStart(findLineNumber(text, allMatchStartFinal)); - moduleNode.getProperties().put("__all__", allExports); - nodes.add(moduleNode); + if (allInfo.exports != null) { + nodes.add(createModuleNode(fp, moduleName, allInfo.exports, + findLineNumber(text, allInfo.matchStart))); } return DetectorResult.of(nodes, edges); @@ -282,18 +188,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { Map> decoratorMap = collectDecorators(text); - Matcher allMatch = ALL_RE.matcher(text); - List allExports = null; - int allMatchStart = -1; - if (allMatch.find()) { - allMatchStart = allMatch.start(); - String raw = allMatch.group(1); - allExports = new ArrayList<>(); - Matcher qm = QUOTED_NAME_RE.matcher(raw); - while (qm.find()) { - allExports.add(qm.group(1)); - } - } + AllExports allInfo = extractAllExports(text); List classRanges = new ArrayList<>(); List classNames = new ArrayList<>(); @@ -311,46 +206,11 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { List annotations = findDecoratorsForLine(decoratorMap, line); - Map properties = new HashMap<>(); - if (basesStr != null && !basesStr.isBlank()) { - List bases = new ArrayList<>(); - for (String b : basesStr.split(",")) { - String trimmed = b.trim(); - if (!trimmed.isEmpty()) { - bases.add(trimmed); - } - } - properties.put("bases", bases); - } - if (allExports != null && allExports.contains(className)) { - properties.put("exported", true); - } + Map properties = buildBaseProperties(basesStr, className, allInfo.exports); String nodeId = "py:" + fp + ":class:" + className; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.CLASS); - node.setLabel(className); - node.setFqn(className); - node.setModule(moduleName); - node.setFilePath(fp); - node.setLineStart(line); - node.setAnnotations(annotations); - node.setProperties(properties); - nodes.add(node); - - if (basesStr != null && !basesStr.isBlank()) { - for (String b : basesStr.split(",")) { - String base = b.trim(); - if (!base.isEmpty()) { - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->extends->" + base); - edge.setKind(EdgeKind.EXTENDS); - edge.setSourceId(nodeId); - edges.add(edge); - } - } - } + nodes.add(createClassNode(nodeId, className, fp, moduleName, line, annotations, properties)); + addExtendsEdges(nodeId, basesStr, edges); } Matcher funcMatcher = FUNC_RE.matcher(text); @@ -367,46 +227,20 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { if (isAsync) { properties.put("async", true); } - if (allExports != null && allExports.contains(funcName)) { + if (allInfo.exports != null && allInfo.exports.contains(funcName)) { properties.put("exported", true); } if (indentLen == 0) { String nodeId = "py:" + fp + ":func:" + funcName; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.METHOD); - node.setLabel(funcName); - node.setFqn(funcName); - node.setModule(moduleName); - node.setFilePath(fp); - node.setLineStart(line); - node.setAnnotations(annotations); - node.setProperties(properties); - nodes.add(node); + nodes.add(createFunctionNode(nodeId, funcName, null, fp, moduleName, line, annotations, properties)); } else { String enclosingClass = findEnclosingClassRegex(classNames, classRanges, line, indentLen); if (enclosingClass != null) { String nodeId = "py:" + fp + ":class:" + enclosingClass + ":method:" + funcName; properties.put("class", enclosingClass); - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.METHOD); - node.setLabel(enclosingClass + "." + funcName); - node.setFqn(enclosingClass + "." + funcName); - node.setModule(moduleName); - node.setFilePath(fp); - node.setLineStart(line); - node.setAnnotations(annotations); - node.setProperties(properties); - nodes.add(node); - - String classNodeId = "py:" + fp + ":class:" + enclosingClass; - CodeEdge edge = new CodeEdge(); - edge.setId(classNodeId + "->defines->" + nodeId); - edge.setKind(EdgeKind.DEFINES); - edge.setSourceId(classNodeId); - edges.add(edge); + nodes.add(createFunctionNode(nodeId, funcName, enclosingClass, fp, moduleName, line, annotations, properties)); + addDefinesEdge("py:" + fp + ":class:" + enclosingClass, nodeId, edges); } } } @@ -416,42 +250,137 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { String fromModule = importMatcher.group(1); String importNames = importMatcher.group(2); if (fromModule != null) { - CodeEdge edge = new CodeEdge(); - edge.setId(fp + "->imports->" + fromModule); - edge.setKind(EdgeKind.IMPORTS); - edge.setSourceId(fp); - edges.add(edge); + addImportEdge(fp, fromModule, edges); } else { for (String name : importNames.split(",")) { String trimmed = name.trim(); if (!trimmed.isEmpty()) { - CodeEdge edge = new CodeEdge(); - edge.setId(fp + "->imports->" + trimmed); - edge.setKind(EdgeKind.IMPORTS); - edge.setSourceId(fp); - edges.add(edge); + addImportEdge(fp, trimmed, edges); } } } } - if (allExports != null) { - String moduleNodeId = "py:" + fp + ":module"; - CodeNode moduleNode = new CodeNode(); - moduleNode.setId(moduleNodeId); - moduleNode.setKind(NodeKind.MODULE); - moduleNode.setLabel(fp); - moduleNode.setFqn(fp); - moduleNode.setModule(moduleName); - moduleNode.setFilePath(fp); - moduleNode.setLineStart(findLineNumber(text, allMatchStart)); - moduleNode.getProperties().put("__all__", allExports); - nodes.add(moduleNode); + if (allInfo.exports != null) { + nodes.add(createModuleNode(fp, moduleName, allInfo.exports, + findLineNumber(text, allInfo.matchStart))); } return DetectorResult.of(nodes, edges); } + // --- Shared node/edge helpers --- + + private record AllExports(List exports, int matchStart) {} + + private static AllExports extractAllExports(String text) { + Matcher allMatch = ALL_RE.matcher(text); + if (allMatch.find()) { + String raw = allMatch.group(1); + List exports = new ArrayList<>(); + Matcher qm = QUOTED_NAME_RE.matcher(raw); + while (qm.find()) { + exports.add(qm.group(1)); + } + return new AllExports(exports, allMatch.start()); + } + return new AllExports(null, -1); + } + + private static Map buildBaseProperties(String basesStr, String className, + List allExports) { + Map properties = new HashMap<>(); + if (basesStr != null && !basesStr.isBlank()) { + List bases = new ArrayList<>(); + for (String b : basesStr.split(",")) { + String trimmed = b.trim(); + if (!trimmed.isEmpty()) { + bases.add(trimmed); + } + } + properties.put("bases", bases); + } + if (allExports != null && allExports.contains(className)) { + properties.put("exported", true); + } + return properties; + } + + private static CodeNode createClassNode(String nodeId, String className, String fp, + String moduleName, int line, List annotations, Map properties) { + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.CLASS); + node.setLabel(className); + node.setFqn(className); + node.setModule(moduleName); + node.setFilePath(fp); + node.setLineStart(line); + node.setAnnotations(annotations); + node.setProperties(properties); + return node; + } + + private static void addExtendsEdges(String nodeId, String basesStr, List edges) { + if (basesStr != null && !basesStr.isBlank()) { + for (String b : basesStr.split(",")) { + String base = b.trim(); + if (!base.isEmpty()) { + CodeEdge edge = new CodeEdge(); + edge.setId(nodeId + "->extends->" + base); + edge.setKind(EdgeKind.EXTENDS); + edge.setSourceId(nodeId); + edges.add(edge); + } + } + } + } + + private static CodeNode createFunctionNode(String nodeId, String funcName, String enclosingClass, + String fp, String moduleName, int line, List annotations, Map properties) { + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.METHOD); + node.setLabel(enclosingClass != null ? enclosingClass + "." + funcName : funcName); + node.setFqn(enclosingClass != null ? enclosingClass + "." + funcName : funcName); + node.setModule(moduleName); + node.setFilePath(fp); + node.setLineStart(line); + node.setAnnotations(annotations); + node.setProperties(properties); + return node; + } + + private static void addDefinesEdge(String classNodeId, String methodNodeId, List edges) { + CodeEdge edge = new CodeEdge(); + edge.setId(classNodeId + "->defines->" + methodNodeId); + edge.setKind(EdgeKind.DEFINES); + edge.setSourceId(classNodeId); + edges.add(edge); + } + + private static void addImportEdge(String fp, String importedName, List edges) { + CodeEdge edge = new CodeEdge(); + edge.setId(fp + "->imports->" + importedName); + edge.setKind(EdgeKind.IMPORTS); + edge.setSourceId(fp); + edges.add(edge); + } + + private static CodeNode createModuleNode(String fp, String moduleName, List allExports, int line) { + String moduleNodeId = "py:" + fp + ":module"; + CodeNode moduleNode = new CodeNode(); + moduleNode.setId(moduleNodeId); + moduleNode.setKind(NodeKind.MODULE); + moduleNode.setLabel(fp); + moduleNode.setFqn(fp); + moduleNode.setModule(moduleName); + moduleNode.setFilePath(fp); + moduleNode.setLineStart(line); + moduleNode.getProperties().put("__all__", allExports); + return moduleNode; + } + // --- Helper methods --- private Map> collectDecorators(String text) { diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/SQLAlchemyModelDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/SQLAlchemyModelDetector.java index 77e62509..43623cee 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/SQLAlchemyModelDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/SQLAlchemyModelDetector.java @@ -70,44 +70,14 @@ public void enterClassdef(Python3Parser.ClassdefContext classCtx) { int line = lineOf(classCtx); String classBody = extractClassBody(text, classCtx); - // Extract table name - Matcher tableMatch = TABLE_NAME.matcher(classBody); - String tableName = tableMatch.find() ? tableMatch.group(1) : className.toLowerCase() + "s"; - - // Extract columns - List columns = new ArrayList<>(); - Matcher colMatcher = COLUMN_PATTERN.matcher(classBody); - while (colMatcher.find()) { - columns.add(colMatcher.group(1)); - } + String tableName = extractTableName(classBody, className); + List columns = extractColumns(classBody); String nodeId = "entity:" + (moduleName != null ? moduleName : "") + ":" + className; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENTITY); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("table_name", tableName); - node.getProperties().put("columns", columns); - node.getProperties().put("framework", "sqlalchemy"); - nodes.add(node); + nodes.add(createEntityNode(nodeId, className, filePath, moduleName, line, tableName, columns)); addDbEdge(nodeId, ctx.registry(), nodes, edges); - // Relationships - Matcher relMatcher = RELATIONSHIP_PATTERN.matcher(classBody); - while (relMatcher.find()) { - String targetClass = relMatcher.group(2); - String targetId = "entity:" + (moduleName != null ? moduleName : "") + ":" + targetClass; - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->maps_to->" + targetId); - edge.setKind(EdgeKind.MAPS_TO); - edge.setSourceId(nodeId); - edge.setTarget(new CodeNode("*:" + targetClass, NodeKind.ENTITY, targetClass)); - edges.add(edge); - } + addRelationshipEdges(classBody, nodeId, moduleName, edges); } }, tree); @@ -139,44 +109,64 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { classBody = text.substring(classStart); } - Matcher tableMatch = TABLE_NAME.matcher(classBody); - String tableName = tableMatch.find() ? tableMatch.group(1) : className.toLowerCase() + "s"; - - List columns = new ArrayList<>(); - Matcher colMatcher = COLUMN_PATTERN.matcher(classBody); - while (colMatcher.find()) { - columns.add(colMatcher.group(1)); - } + String tableName = extractTableName(classBody, className); + List columns = extractColumns(classBody); String nodeId = "entity:" + (moduleName != null ? moduleName : "") + ":" + className; - CodeNode node = new CodeNode(); - node.setId(nodeId); - node.setKind(NodeKind.ENTITY); - node.setLabel(className); - node.setFqn(filePath + "::" + className); - node.setModule(moduleName); - node.setFilePath(filePath); - node.setLineStart(line); - node.getProperties().put("table_name", tableName); - node.getProperties().put("columns", columns); - node.getProperties().put("framework", "sqlalchemy"); - nodes.add(node); + nodes.add(createEntityNode(nodeId, className, filePath, moduleName, line, tableName, columns)); addDbEdge(nodeId, ctx.registry(), nodes, edges); - Matcher relMatcher = RELATIONSHIP_PATTERN.matcher(classBody); - while (relMatcher.find()) { - String targetClass = relMatcher.group(2); - String targetId = "entity:" + (moduleName != null ? moduleName : "") + ":" + targetClass; - CodeEdge edge = new CodeEdge(); - edge.setId(nodeId + "->maps_to->" + targetId); - edge.setKind(EdgeKind.MAPS_TO); - edge.setSourceId(nodeId); - edge.setTarget(new CodeNode("*:" + targetClass, NodeKind.ENTITY, targetClass)); - edges.add(edge); - } + addRelationshipEdges(classBody, nodeId, moduleName, edges); } return DetectorResult.of(nodes, edges); } + // --- Shared helpers --- + + private static String extractTableName(String classBody, String className) { + Matcher tableMatch = TABLE_NAME.matcher(classBody); + return tableMatch.find() ? tableMatch.group(1) : className.toLowerCase() + "s"; + } + + private static List extractColumns(String classBody) { + List columns = new ArrayList<>(); + Matcher colMatcher = COLUMN_PATTERN.matcher(classBody); + while (colMatcher.find()) { + columns.add(colMatcher.group(1)); + } + return columns; + } + + private static CodeNode createEntityNode(String nodeId, String className, String filePath, + String moduleName, int line, String tableName, List columns) { + CodeNode node = new CodeNode(); + node.setId(nodeId); + node.setKind(NodeKind.ENTITY); + node.setLabel(className); + node.setFqn(filePath + "::" + className); + node.setModule(moduleName); + node.setFilePath(filePath); + node.setLineStart(line); + node.getProperties().put("table_name", tableName); + node.getProperties().put("columns", columns); + node.getProperties().put("framework", "sqlalchemy"); + return node; + } + + private static void addRelationshipEdges(String classBody, String nodeId, String moduleName, + List edges) { + Matcher relMatcher = RELATIONSHIP_PATTERN.matcher(classBody); + while (relMatcher.find()) { + String targetClass = relMatcher.group(2); + String targetId = "entity:" + (moduleName != null ? moduleName : "") + ":" + targetClass; + CodeEdge edge = new CodeEdge(); + edge.setId(nodeId + "->maps_to->" + targetId); + edge.setKind(EdgeKind.MAPS_TO); + edge.setSourceId(nodeId); + edge.setTarget(new CodeNode("*:" + targetClass, NodeKind.ENTITY, targetClass)); + edges.add(edge); + } + } + } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/scala/ScalaStructuresDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/scala/ScalaStructuresDetector.java index fbb2aefd..d84ca0e7 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/scala/ScalaStructuresDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/scala/ScalaStructuresDetector.java @@ -4,6 +4,7 @@ import io.github.randomcodespace.iq.grammar.AntlrParserFactory; import io.github.randomcodespace.iq.detector.DetectorContext; import io.github.randomcodespace.iq.detector.DetectorResult; +import io.github.randomcodespace.iq.detector.StructuresDetectorHelper; import io.github.randomcodespace.iq.model.CodeEdge; import io.github.randomcodespace.iq.model.CodeNode; import io.github.randomcodespace.iq.model.EdgeKind; @@ -59,10 +60,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { Matcher m = IMPORT_RE.matcher(text); while (m.find()) { - CodeEdge e = new CodeEdge(); e.setId(fp + ":imports:" + m.group(1)); - e.setKind(EdgeKind.IMPORTS); e.setSourceId(fp); - e.setTarget(new CodeNode(m.group(1), NodeKind.MODULE, m.group(1))); - edges.add(e); + StructuresDetectorHelper.addImportEdge(fp, m.group(1), edges); } m = CLASS_RE.matcher(text); @@ -71,24 +69,15 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { String baseClass = m.group(2); String traitsStr = m.group(3); String nodeId = fp + ":" + className; - CodeNode n = new CodeNode(); n.setId(nodeId); - n.setKind(NodeKind.CLASS); n.setLabel(className); n.setFqn(className); - n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); - nodes.add(n); + nodes.add(StructuresDetectorHelper.createStructureNode(fp, className, NodeKind.CLASS, findLineNumber(text, m.start()))); if (baseClass != null) { - CodeEdge e = new CodeEdge(); e.setId(nodeId + ":extends:" + baseClass); - e.setKind(EdgeKind.EXTENDS); e.setSourceId(nodeId); - e.setTarget(new CodeNode(baseClass, NodeKind.CLASS, baseClass)); - edges.add(e); + StructuresDetectorHelper.addExtendsEdge(nodeId, baseClass, NodeKind.CLASS, edges); } if (traitsStr != null) { for (String trait : traitsStr.split(",")) { trait = trait.trim(); if (!trait.isEmpty()) { - CodeEdge e = new CodeEdge(); e.setId(nodeId + ":implements:" + trait); - e.setKind(EdgeKind.IMPLEMENTS); e.setSourceId(nodeId); - e.setTarget(new CodeNode(trait, NodeKind.INTERFACE, trait)); - edges.add(e); + StructuresDetectorHelper.addImplementsEdge(nodeId, trait, edges); } } } @@ -96,31 +85,21 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { m = TRAIT_RE.matcher(text); while (m.find()) { - String name = m.group(1); - CodeNode n = new CodeNode(); n.setId(fp + ":" + name); - n.setKind(NodeKind.INTERFACE); n.setLabel(name); n.setFqn(name); - n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); + CodeNode n = StructuresDetectorHelper.createStructureNode(fp, m.group(1), NodeKind.INTERFACE, findLineNumber(text, m.start())); n.getProperties().put("type", "trait"); nodes.add(n); } m = OBJECT_RE.matcher(text); while (m.find()) { - String name = m.group(1); - CodeNode n = new CodeNode(); n.setId(fp + ":" + name); - n.setKind(NodeKind.CLASS); n.setLabel(name); n.setFqn(name); - n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); + CodeNode n = StructuresDetectorHelper.createStructureNode(fp, m.group(1), NodeKind.CLASS, findLineNumber(text, m.start())); n.getProperties().put("type", "object"); nodes.add(n); } m = DEF_RE.matcher(text); while (m.find()) { - String name = m.group(1); - CodeNode n = new CodeNode(); n.setId(fp + ":" + name); - n.setKind(NodeKind.METHOD); n.setLabel(name); n.setFqn(name); - n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); - nodes.add(n); + nodes.add(StructuresDetectorHelper.createStructureNode(fp, m.group(1), NodeKind.METHOD, findLineNumber(text, m.start()))); } return DetectorResult.of(nodes, edges);