diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java b/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java index 2093b74a..8b1430e8 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java @@ -61,6 +61,10 @@ */ @Service public class Analyzer { + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_ROOT = "root"; + private static final String PROP_SERVICE = "service"; + private static final Logger log = LoggerFactory.getLogger(Analyzer.class); @@ -335,7 +339,7 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach // 5b. Detect service boundaries and create SERVICE nodes report.accept("Detecting service boundaries..."); var serviceDetector = new ServiceDetector(); - String projectDirName = root.getFileName() != null ? root.getFileName().toString() : "root"; + String projectDirName = root.getFileName() != null ? root.getFileName().toString() : PROP_ROOT; var serviceResult = serviceDetector.detect(allNodes, builder.getEdges(), projectDirName, root); if (!serviceResult.serviceNodes().isEmpty()) { serviceResult.serviceNodes().forEach(n -> n.setProvenance(builder.getProvenance())); @@ -348,7 +352,7 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach String serviceName = config.getServiceName(); if (serviceName != null && !serviceName.isBlank()) { for (CodeNode node : allNodes) { - node.getProperties().put("service", serviceName); + node.getProperties().put(PROP_SERVICE, serviceName); } } @@ -381,7 +385,7 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach // 7b. Compute framework breakdown from node properties Map frameworkBreakdown = new HashMap<>(); for (CodeNode node : allNodes) { - Object fw = node.getProperties().get("framework"); + Object fw = node.getProperties().get(PROP_FRAMEWORK); if (fw != null && !fw.toString().isEmpty()) { frameworkBreakdown.merge(fw.toString(), 1, Integer::sum); } @@ -611,13 +615,13 @@ private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int b String svcName = config.getServiceName(); if (svcName != null && !svcName.isBlank()) { for (CodeNode node : result.nodes()) { - node.getProperties().put("service", svcName); + node.getProperties().put(PROP_SERVICE, svcName); } } // Track breakdowns for (CodeNode node : result.nodes()) { nodeBreakdown.merge(node.getKind().getValue(), 1, Integer::sum); - Object fw = node.getProperties().get("framework"); + Object fw = node.getProperties().get(PROP_FRAMEWORK); if (fw != null && !fw.toString().isEmpty()) { frameworkBreakdown.merge(fw.toString(), 1, Integer::sum); } @@ -971,12 +975,12 @@ private int[] processSmartBatch( String svcName = config.getServiceName(); if (svcName != null && !svcName.isBlank()) { for (CodeNode node : result.nodes()) { - node.getProperties().put("service", svcName); + node.getProperties().put(PROP_SERVICE, svcName); } } for (CodeNode node : result.nodes()) { nodeBreakdown.merge(node.getKind().getValue(), 1, Integer::sum); - Object fw = node.getProperties().get("framework"); + Object fw = node.getProperties().get(PROP_FRAMEWORK); if (fw != null && !fw.toString().isEmpty()) { frameworkBreakdown.merge(fw.toString(), 1, Integer::sum); } @@ -1001,7 +1005,7 @@ private int[] processSmartBatch( * Partition discovered files into modules based on build-file boundary markers. *

* Files are assigned to the deepest module directory that contains them. - * Files with no matching module are placed in a {@code "root"} partition. + * Files with no matching module are placed in a {@code PROP_ROOT} partition. * The returned map is a {@link TreeMap} for deterministic iteration. * * @param root absolute repository root (used only for logging) @@ -1021,7 +1025,7 @@ Map> detectModules(Path root, List // If no module boundaries found, treat everything as root if (moduleDirs.isEmpty()) { Map> single = new TreeMap<>(); - single.put("root", new ArrayList<>(files)); + single.put(PROP_ROOT, new ArrayList<>(files)); return single; } @@ -1045,7 +1049,7 @@ Map> detectModules(Path root, List } } - String key = bestModule != null ? bestModule : "root"; + String key = bestModule != null ? bestModule : PROP_ROOT; result.computeIfAbsent(key, k -> new ArrayList<>()).add(file); } diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java b/src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java index 12fbd486..9570a8d5 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/ConfigScanner.java @@ -34,6 +34,22 @@ */ @Component public class ConfigScanner { + private static final String PROP_DEPENDENCY = "dependency"; + private static final String PROP_DETECTION = "detection"; + private static final String PROP_ELASTICSEARCH = "elasticsearch"; + private static final String PROP_H2 = "h2"; + private static final String PROP_KAFKA = "kafka"; + private static final String PROP_MARIADB = "mariadb"; + private static final String PROP_MONGO = "mongo"; + private static final String PROP_MONGODB = "mongodb"; + private static final String PROP_MYSQL = "mysql"; + private static final String PROP_POM_XML = "pom.xml"; + private static final String PROP_POSTGRESQL = "postgresql"; + private static final String PROP_RABBITMQ = "rabbitmq"; + private static final String PROP_REDIS = "redis"; + private static final String PROP_SOURCE = "source"; + private static final String PROP_SQL = "sql"; + private static final Logger log = LoggerFactory.getLogger(ConfigScanner.class); @@ -152,7 +168,7 @@ private void processSpringFlatMap(Map flat, InfrastructureRegist "spring.datasource", dbType, dsUrl, - Map.of("source", "spring.datasource.url"))); + Map.of(PROP_SOURCE, "spring.datasource.url"))); } // Kafka bootstrap-servers @@ -164,9 +180,9 @@ private void processSpringFlatMap(Map flat, InfrastructureRegist "topic:spring.kafka", InfraEndpoint.Kind.TOPIC, "spring.kafka", - "kafka", + PROP_KAFKA, kafkaServers, - Map.of("source", "spring.kafka.bootstrap-servers"))); + Map.of(PROP_SOURCE, "spring.kafka.bootstrap-servers"))); } // Redis (spring.data.redis or spring.redis) @@ -182,9 +198,9 @@ private void processSpringFlatMap(Map flat, InfrastructureRegist "cache:spring.redis", InfraEndpoint.Kind.CACHE, "spring.redis", - "redis", + PROP_REDIS, "redis://" + redisHost + ":" + redisPort, - Map.of("source", "spring.redis.host"))); + Map.of(PROP_SOURCE, "spring.redis.host"))); } // RabbitMQ @@ -195,9 +211,9 @@ private void processSpringFlatMap(Map flat, InfrastructureRegist "queue:spring.rabbitmq", InfraEndpoint.Kind.QUEUE, "spring.rabbitmq", - "rabbitmq", + PROP_RABBITMQ, "amqp://" + rabbitHost + ":" + rabbitPort, - Map.of("source", "spring.rabbitmq.host"))); + Map.of(PROP_SOURCE, "spring.rabbitmq.host"))); } } @@ -265,36 +281,36 @@ private void detectDockerInfra(String svcName, String image, String ports, Map props = new TreeMap<>(); props.put("image", image); - props.put("source", "docker-compose"); + props.put(PROP_SOURCE, "docker-compose"); if (ports != null) props.put("ports", ports); if (imageBase.contains("postgres")) { registry.register(new InfraEndpoint("db:compose:" + svcName, - InfraEndpoint.Kind.DATABASE, svcName, "postgresql", null, props)); - } else if (imageBase.contains("mariadb")) { + InfraEndpoint.Kind.DATABASE, svcName, PROP_POSTGRESQL, null, props)); + } else if (imageBase.contains(PROP_MARIADB)) { registry.register(new InfraEndpoint("db:compose:" + svcName, - InfraEndpoint.Kind.DATABASE, svcName, "mariadb", null, props)); - } else if (imageBase.contains("mysql")) { + InfraEndpoint.Kind.DATABASE, svcName, PROP_MARIADB, null, props)); + } else if (imageBase.contains(PROP_MYSQL)) { registry.register(new InfraEndpoint("db:compose:" + svcName, - InfraEndpoint.Kind.DATABASE, svcName, "mysql", null, props)); - } else if (imageBase.contains("mongo")) { + InfraEndpoint.Kind.DATABASE, svcName, PROP_MYSQL, null, props)); + } else if (imageBase.contains(PROP_MONGO)) { registry.register(new InfraEndpoint("db:compose:" + svcName, - InfraEndpoint.Kind.DATABASE, svcName, "mongodb", null, props)); - } else if (imageBase.contains("redis")) { + InfraEndpoint.Kind.DATABASE, svcName, PROP_MONGODB, null, props)); + } else if (imageBase.contains(PROP_REDIS)) { registry.register(new InfraEndpoint("cache:compose:" + svcName, - InfraEndpoint.Kind.CACHE, svcName, "redis", null, props)); - } else if (imageBase.contains("kafka") || imageBase.contains("cp-kafka")) { + InfraEndpoint.Kind.CACHE, svcName, PROP_REDIS, null, props)); + } else if (imageBase.contains(PROP_KAFKA) || imageBase.contains("cp-kafka")) { registry.register(new InfraEndpoint("topic:compose:" + svcName, - InfraEndpoint.Kind.TOPIC, svcName, "kafka", null, props)); - } else if (imageBase.contains("rabbitmq")) { + InfraEndpoint.Kind.TOPIC, svcName, PROP_KAFKA, null, props)); + } else if (imageBase.contains(PROP_RABBITMQ)) { registry.register(new InfraEndpoint("queue:compose:" + svcName, - InfraEndpoint.Kind.QUEUE, svcName, "rabbitmq", null, props)); + InfraEndpoint.Kind.QUEUE, svcName, PROP_RABBITMQ, null, props)); } else if (imageBase.contains("opensearch")) { registry.register(new InfraEndpoint("db:compose:" + svcName, InfraEndpoint.Kind.DATABASE, svcName, "opensearch", null, props)); - } else if (imageBase.contains("elasticsearch")) { + } else if (imageBase.contains(PROP_ELASTICSEARCH)) { registry.register(new InfraEndpoint("db:compose:" + svcName, - InfraEndpoint.Kind.DATABASE, svcName, "elasticsearch", null, props)); + InfraEndpoint.Kind.DATABASE, svcName, PROP_ELASTICSEARCH, null, props)); } else if (imageBase.contains("cassandra")) { registry.register(new InfraEndpoint("db:compose:" + svcName, InfraEndpoint.Kind.DATABASE, svcName, "cassandra", null, props)); @@ -360,7 +376,7 @@ private void processEnvVars(Map env, InfrastructureRegistry regi if (!registry.getDatabases().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.DATABASE, "database_url", detectDatabaseTypeFromUrl(dbUrl), dbUrl, - Map.of("source", ".env"))); + Map.of(PROP_SOURCE, ".env"))); } } else { // DB_HOST fallback @@ -370,8 +386,8 @@ private void processEnvVars(Map env, InfrastructureRegistry regi String id = "db:env:db_host"; if (!registry.getDatabases().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.DATABASE, - "database", "postgresql", dbHost + ":" + dbPort, - Map.of("source", ".env"))); + "database", PROP_POSTGRESQL, dbHost + ":" + dbPort, + Map.of(PROP_SOURCE, ".env"))); } } } @@ -382,7 +398,7 @@ private void processEnvVars(Map env, InfrastructureRegistry regi String id = "cache:env:redis_url"; if (!registry.getCaches().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.CACHE, - "redis", "redis", redisUrl, Map.of("source", ".env"))); + PROP_REDIS, PROP_REDIS, redisUrl, Map.of(PROP_SOURCE, ".env"))); } } else { String redisHost = env.get("REDIS_HOST"); @@ -391,8 +407,8 @@ private void processEnvVars(Map env, InfrastructureRegistry regi String id = "cache:env:redis_host"; if (!registry.getCaches().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.CACHE, - "redis", "redis", "redis://" + redisHost + ":" + redisPort, - Map.of("source", ".env"))); + PROP_REDIS, PROP_REDIS, "redis://" + redisHost + ":" + redisPort, + Map.of(PROP_SOURCE, ".env"))); } } } @@ -406,7 +422,7 @@ private void processEnvVars(Map env, InfrastructureRegistry regi String id = "topic:env:kafka_brokers"; if (!registry.getTopics().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.TOPIC, - "kafka", "kafka", kafkaBrokers, Map.of("source", ".env"))); + PROP_KAFKA, PROP_KAFKA, kafkaBrokers, Map.of(PROP_SOURCE, ".env"))); } } } @@ -416,7 +432,7 @@ private void processEnvVars(Map env, InfrastructureRegistry regi // ------------------------------------------------------------------------- private void scanBuildFiles(Path root, InfrastructureRegistry registry) { - Path pomXml = root.resolve("pom.xml"); + Path pomXml = root.resolve(PROP_POM_XML); if (Files.isRegularFile(pomXml)) { parsePomXml(pomXml, registry); } @@ -428,26 +444,26 @@ private void parsePomXml(Path file, InfrastructureRegistry registry) { Set artifactIds = extractPomArtifactIds(content); boolean hasJpa = artifactIds.stream().anyMatch(a -> a.contains("jpa") || a.contains("hibernate") || a.contains("jdbc")); - boolean hasPostgres = artifactIds.contains("postgresql"); + boolean hasPostgres = artifactIds.contains(PROP_POSTGRESQL); boolean hasMysql = artifactIds.stream().anyMatch(a -> a.contains("mysql-connector")); - boolean hasH2 = artifactIds.contains("h2"); - boolean hasMongo = artifactIds.stream().anyMatch(a -> a.contains("mongo")); - boolean hasKafka = artifactIds.stream().anyMatch(a -> a.contains("kafka")); - boolean hasRedis = artifactIds.stream().anyMatch(a -> a.contains("redis") || a.equals("lettuce-core") || a.equals("jedis")); - boolean hasRabbit = artifactIds.stream().anyMatch(a -> a.contains("amqp") || a.contains("rabbitmq")); - boolean hasElastic = artifactIds.stream().anyMatch(a -> a.contains("elasticsearch")); + boolean hasH2 = artifactIds.contains(PROP_H2); + boolean hasMongo = artifactIds.stream().anyMatch(a -> a.contains(PROP_MONGO)); + boolean hasKafka = artifactIds.stream().anyMatch(a -> a.contains(PROP_KAFKA)); + boolean hasRedis = artifactIds.stream().anyMatch(a -> a.contains(PROP_REDIS) || a.equals("lettuce-core") || a.equals("jedis")); + boolean hasRabbit = artifactIds.stream().anyMatch(a -> a.contains("amqp") || a.contains(PROP_RABBITMQ)); + boolean hasElastic = artifactIds.stream().anyMatch(a -> a.contains(PROP_ELASTICSEARCH)); if (hasJpa || hasPostgres || hasMysql || hasH2 || hasMongo) { - String dbType = hasPostgres ? "postgresql" - : hasMysql ? "mysql" - : hasMongo ? "mongodb" - : hasH2 ? "h2" - : "sql"; + String dbType = hasPostgres ? PROP_POSTGRESQL + : hasMysql ? PROP_MYSQL + : hasMongo ? PROP_MONGODB + : hasH2 ? PROP_H2 + : PROP_SQL; String id = "db:pom:" + dbType; if (!registry.getDatabases().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.DATABASE, dbType, dbType, null, - Map.of("source", "pom.xml", "detection", "dependency"))); + Map.of(PROP_SOURCE, PROP_POM_XML, PROP_DETECTION, PROP_DEPENDENCY))); } } @@ -455,8 +471,8 @@ private void parsePomXml(Path file, InfrastructureRegistry registry) { String id = "topic:pom:kafka"; if (!registry.getTopics().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.TOPIC, - "kafka", "kafka", null, - Map.of("source", "pom.xml", "detection", "dependency"))); + PROP_KAFKA, PROP_KAFKA, null, + Map.of(PROP_SOURCE, PROP_POM_XML, PROP_DETECTION, PROP_DEPENDENCY))); } } @@ -464,8 +480,8 @@ private void parsePomXml(Path file, InfrastructureRegistry registry) { String id = "cache:pom:redis"; if (!registry.getCaches().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.CACHE, - "redis", "redis", null, - Map.of("source", "pom.xml", "detection", "dependency"))); + PROP_REDIS, PROP_REDIS, null, + Map.of(PROP_SOURCE, PROP_POM_XML, PROP_DETECTION, PROP_DEPENDENCY))); } } @@ -473,8 +489,8 @@ private void parsePomXml(Path file, InfrastructureRegistry registry) { String id = "queue:pom:rabbitmq"; if (!registry.getQueues().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.QUEUE, - "rabbitmq", "rabbitmq", null, - Map.of("source", "pom.xml", "detection", "dependency"))); + PROP_RABBITMQ, PROP_RABBITMQ, null, + Map.of(PROP_SOURCE, PROP_POM_XML, PROP_DETECTION, PROP_DEPENDENCY))); } } @@ -482,8 +498,8 @@ private void parsePomXml(Path file, InfrastructureRegistry registry) { String id = "db:pom:elasticsearch"; if (!registry.getDatabases().containsKey(id)) { registry.register(new InfraEndpoint(id, InfraEndpoint.Kind.DATABASE, - "elasticsearch", "elasticsearch", null, - Map.of("source", "pom.xml", "detection", "dependency"))); + PROP_ELASTICSEARCH, PROP_ELASTICSEARCH, null, + Map.of(PROP_SOURCE, PROP_POM_XML, PROP_DETECTION, PROP_DEPENDENCY))); } } } catch (Exception e) { @@ -505,17 +521,17 @@ private Set extractPomArtifactIds(String content) { // ------------------------------------------------------------------------- private String detectDatabaseTypeFromUrl(String url) { - if (url == null) return "sql"; + if (url == null) return PROP_SQL; String lower = url.toLowerCase(); - if (lower.contains("postgresql") || lower.contains("postgres")) return "postgresql"; - if (lower.contains("mysql")) return "mysql"; - if (lower.contains("mariadb")) return "mariadb"; + if (lower.contains(PROP_POSTGRESQL) || lower.contains("postgres")) return PROP_POSTGRESQL; + if (lower.contains(PROP_MYSQL)) return PROP_MYSQL; + if (lower.contains(PROP_MARIADB)) return PROP_MARIADB; if (lower.contains("oracle")) return "oracle"; if (lower.contains("sqlserver") || lower.contains("mssql")) return "sqlserver"; - if (lower.contains("h2")) return "h2"; + if (lower.contains(PROP_H2)) return PROP_H2; if (lower.contains("sqlite")) return "sqlite"; - if (lower.contains("mongodb") || lower.contains("mongo")) return "mongodb"; - return "sql"; + if (lower.contains(PROP_MONGODB) || lower.contains(PROP_MONGO)) return PROP_MONGODB; + return PROP_SQL; } private static String coalesce(String... values) { diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/LayerClassifier.java b/src/main/java/io/github/randomcodespace/iq/analyzer/LayerClassifier.java index 740498cc..3f6a4c39 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/LayerClassifier.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/LayerClassifier.java @@ -17,6 +17,10 @@ */ @Service public class LayerClassifier { + private static final String PROP_BACKEND = "backend"; + private static final String PROP_FRONTEND = "frontend"; + private static final String PROP_UNKNOWN = "unknown"; + private static final Set FRONTEND_NODE_KINDS = Set.of( NodeKind.COMPONENT, NodeKind.HOOK @@ -67,7 +71,7 @@ public class LayerClassifier { "asp.net", "koa", "hapi", "fastify" ); - // -- Fallback path heuristics (applied only to "unknown" nodes) -- + // -- Fallback path heuristics (applied only to PROP_UNKNOWN nodes) -- private static final Pattern BACKEND_PACKAGE_PATH_RE = Pattern.compile( "(?:^|/|\\.)(?:controller|controllers|api|web|rest|resource|resources|" @@ -103,8 +107,8 @@ public void classify(List nodes) { */ String classifyOne(CodeNode node) { // 1. Node kind rules - if (FRONTEND_NODE_KINDS.contains(node.getKind())) return "frontend"; - if (BACKEND_NODE_KINDS.contains(node.getKind())) return "backend"; + if (FRONTEND_NODE_KINDS.contains(node.getKind())) return PROP_FRONTEND; + if (BACKEND_NODE_KINDS.contains(node.getKind())) return PROP_BACKEND; if (INFRA_NODE_KINDS.contains(node.getKind())) return "infra"; // 2. Language rules @@ -115,21 +119,21 @@ String classifyOne(CodeNode node) { // 3. File path rules String filePath = node.getFilePath() != null ? node.getFilePath() : ""; - if (FRONTEND_EXT_RE.matcher(filePath).find()) return "frontend"; - if (FRONTEND_PATH_RE.matcher(filePath).find()) return "frontend"; - if (BACKEND_PATH_RE.matcher(filePath).find()) return "backend"; + if (FRONTEND_EXT_RE.matcher(filePath).find()) return PROP_FRONTEND; + if (FRONTEND_PATH_RE.matcher(filePath).find()) return PROP_FRONTEND; + if (BACKEND_PATH_RE.matcher(filePath).find()) return PROP_BACKEND; // 4. Framework rules Object fw = node.getProperties().get("framework"); if (fw instanceof String fwStr) { - if (FRONTEND_FRAMEWORKS.contains(fwStr)) return "frontend"; - if (BACKEND_FRAMEWORKS.contains(fwStr)) return "backend"; + if (FRONTEND_FRAMEWORKS.contains(fwStr)) return PROP_FRONTEND; + if (BACKEND_FRAMEWORKS.contains(fwStr)) return PROP_BACKEND; } // 5. Shared node kinds if (SHARED_NODE_KINDS.contains(node.getKind())) return "shared"; - // 6. Fallback: package/path heuristics for remaining "unknown" nodes + // 6. Fallback: package/path heuristics for remaining PROP_UNKNOWN nodes return classifyByPathFallback(node); } @@ -145,10 +149,10 @@ private String classifyByPathFallback(CodeNode node) { String combined = filePath + "|" + nodeId; // Check frontend path patterns first (components, views, pages, etc.) - if (FRONTEND_PACKAGE_PATH_RE.matcher(combined).find()) return "frontend"; + if (FRONTEND_PACKAGE_PATH_RE.matcher(combined).find()) return PROP_FRONTEND; // Check backend path patterns (controller, model, repository, service, etc.) - if (BACKEND_PACKAGE_PATH_RE.matcher(combined).find()) return "backend"; + if (BACKEND_PACKAGE_PATH_RE.matcher(combined).find()) return PROP_BACKEND; // Check shared path patterns (config, util, common, etc.) if (SHARED_PACKAGE_PATH_RE.matcher(combined).find()) return "shared"; @@ -158,7 +162,7 @@ private String classifyByPathFallback(CodeNode node) { return classifyJavaByPath(filePath); } - return "unknown"; + return PROP_UNKNOWN; } /** @@ -168,8 +172,8 @@ private String classifyByPathFallback(CodeNode node) { private String classifyJavaByPath(String filePath) { // Files in src/main/java or src/main/kotlin are virtually always backend if (filePath.contains("src/main/java/") || filePath.contains("src/main/kotlin/")) { - return "backend"; + return PROP_BACKEND; } - return "unknown"; + return PROP_UNKNOWN; } } diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java b/src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java index 0e8a614c..046c9957 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java @@ -40,6 +40,16 @@ * */ public class ServiceDetector { + private static final String PROP_DJANGO = "django"; + private static final String PROP_DOCKER = "docker"; + private static final String PROP_DOTNET = "dotnet"; + private static final String PROP_GRADLE = "gradle"; + private static final String PROP_HASKELL = "haskell"; + private static final String PROP_PYPROJECT_TOML = "pyproject.toml"; + private static final String PROP_PYTHON = "python"; + private static final String PROP_RUBY = "ruby"; + private static final String PROP_SETUP_PY = "setup.py"; + private static final Logger log = LoggerFactory.getLogger(ServiceDetector.class); @@ -50,10 +60,10 @@ public class ServiceDetector { private static final Map BUILD_FILES = Map.ofEntries( // Java/JVM Map.entry("pom.xml", "maven"), - Map.entry("build.gradle", "gradle"), - Map.entry("build.gradle.kts", "gradle"), - Map.entry("settings.gradle", "gradle"), - Map.entry("settings.gradle.kts", "gradle"), + Map.entry("build.gradle", PROP_GRADLE), + Map.entry("build.gradle.kts", PROP_GRADLE), + Map.entry("settings.gradle", PROP_GRADLE), + Map.entry("settings.gradle.kts", PROP_GRADLE), Map.entry("build.xml", "ant"), Map.entry("build.sbt", "sbt"), Map.entry("project.clj", "leiningen"), @@ -66,18 +76,18 @@ public class ServiceDetector { // Rust Map.entry("Cargo.toml", "cargo"), // Python - Map.entry("pyproject.toml", "python"), - Map.entry("setup.py", "python"), - Map.entry("setup.cfg", "python"), - Map.entry("Pipfile", "python"), - Map.entry("requirements.txt", "python"), - Map.entry("manage.py", "django"), + Map.entry(PROP_PYPROJECT_TOML, PROP_PYTHON), + Map.entry(PROP_SETUP_PY, PROP_PYTHON), + Map.entry("setup.cfg", PROP_PYTHON), + Map.entry("Pipfile", PROP_PYTHON), + Map.entry("requirements.txt", PROP_PYTHON), + Map.entry("manage.py", PROP_DJANGO), // Ruby - Map.entry("Gemfile", "ruby"), + Map.entry("Gemfile", PROP_RUBY), // PHP Map.entry("composer.json", "php"), // .NET (csproj handled by suffix match below) - Map.entry("Directory.Build.props", "dotnet"), + Map.entry("Directory.Build.props", PROP_DOTNET), // Swift Map.entry("Package.swift", "swift"), // Elixir @@ -86,7 +96,7 @@ public class ServiceDetector { Map.entry("pubspec.yaml", "dart"), // Scala (also build.sbt above) // Haskell - Map.entry("stack.yaml", "haskell"), + Map.entry("stack.yaml", PROP_HASKELL), // Zig Map.entry("build.zig", "zig"), // OCaml @@ -102,11 +112,11 @@ public class ServiceDetector { Map.entry("turbo.json", "turbo"), Map.entry("rush.json", "rush"), // Docker (supplemental -- doesn't override other tools) - Map.entry("Dockerfile", "docker"), - Map.entry("docker-compose.yml", "docker"), - Map.entry("docker-compose.yaml", "docker"), - Map.entry("compose.yml", "docker"), - Map.entry("compose.yaml", "docker") + Map.entry("Dockerfile", PROP_DOCKER), + Map.entry("docker-compose.yml", PROP_DOCKER), + Map.entry("docker-compose.yaml", PROP_DOCKER), + Map.entry("compose.yml", PROP_DOCKER), + Map.entry("compose.yaml", PROP_DOCKER) ); /** File extensions matched by suffix (not exact name). */ @@ -119,12 +129,12 @@ public class ServiceDetector { /** Build tools that are supplemental (don't override real build tools). */ private static final java.util.Set SUPPLEMENTAL_TOOLS = java.util.Set.of( - "docker", "nx", "lerna", "turbo", "rush" + PROP_DOCKER, "nx", "lerna", "turbo", "rush" ); /** Python build files ranked by priority (first match wins for a directory). */ private static final List PYTHON_BUILD_FILES = List.of( - "pyproject.toml", "setup.py", "requirements.txt", "manage.py" + PROP_PYPROJECT_TOML, PROP_SETUP_PY, "requirements.txt", "manage.py" ); /** Regex patterns for extracting names from build file contents. */ @@ -199,11 +209,11 @@ public ServiceDetectionResult detect(List nodes, List edges, } if (fileName.endsWith(CSPROJ_EXTENSION) || fileName.endsWith(FSPROJ_EXTENSION) || fileName.endsWith(VBPROJ_EXTENSION)) { - modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "dotnet", fileName)); + modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, PROP_DOTNET, fileName)); } else if (fileName.endsWith(GEMSPEC_EXTENSION)) { - modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "ruby", fileName)); + modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, PROP_RUBY, fileName)); } else if (fileName.endsWith(CABAL_EXTENSION)) { - modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "haskell", fileName)); + modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, PROP_HASKELL, fileName)); } else if (fileName.endsWith(NIMBLE_EXTENSION)) { modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "nim", fileName)); } @@ -356,11 +366,11 @@ public java.nio.file.FileVisitResult visitFile(Path file, java.nio.file.attribut if (name.endsWith(CSPROJ_EXTENSION) || name.endsWith(FSPROJ_EXTENSION) || name.endsWith(VBPROJ_EXTENSION)) { - modules.putIfAbsent(relDir, new ModuleInfo(relDir, "dotnet", name)); + modules.putIfAbsent(relDir, new ModuleInfo(relDir, PROP_DOTNET, name)); } else if (name.endsWith(GEMSPEC_EXTENSION)) { - modules.putIfAbsent(relDir, new ModuleInfo(relDir, "ruby", name)); + modules.putIfAbsent(relDir, new ModuleInfo(relDir, PROP_RUBY, name)); } else if (name.endsWith(CABAL_EXTENSION)) { - modules.putIfAbsent(relDir, new ModuleInfo(relDir, "haskell", name)); + modules.putIfAbsent(relDir, new ModuleInfo(relDir, PROP_HASKELL, name)); } else if (name.endsWith(NIMBLE_EXTENSION)) { modules.putIfAbsent(relDir, new ModuleInfo(relDir, "nim", name)); } else { @@ -436,16 +446,16 @@ private String readNameFromBuildFile(Path projectRoot, String dir, ModuleInfo in String content = Files.readString(buildFile, StandardCharsets.UTF_8); return switch (info.buildTool()) { case "maven" -> extractFromPom(content); - case "gradle" -> extractFromGradleSettings(content, info.buildFile()); + case PROP_GRADLE -> extractFromGradleSettings(content, info.buildFile()); case "npm" -> extractFromPackageJson(content); case "go" -> extractFromGoMod(content); case "cargo" -> extractFromCargoToml(content); - case "python" -> extractFromPythonBuild(content, info.buildFile()); + case PROP_PYTHON -> extractFromPythonBuild(content, info.buildFile()); case "sbt" -> extractWithPattern(content, SBT_NAME); case "php" -> extractWithPattern(content, COMPOSER_NAME); case "elixir" -> extractWithPattern(content, MIX_APP_NAME); case "dart" -> extractWithPattern(content, PUBSPEC_NAME); - case "django" -> null; + case PROP_DJANGO -> null; default -> null; }; } catch (IOException e) { diff --git a/src/main/java/io/github/randomcodespace/iq/analyzer/StructuredParser.java b/src/main/java/io/github/randomcodespace/iq/analyzer/StructuredParser.java index c9a0cd49..5448c7ad 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/StructuredParser.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/StructuredParser.java @@ -24,6 +24,9 @@ */ @Service public class StructuredParser { + private static final String PROP_DATA = "data"; + private static final String PROP_TYPE = "type"; + private static final Logger log = LoggerFactory.getLogger(StructuredParser.class); @@ -70,10 +73,10 @@ private Object parseYaml(String content) { Map result = new LinkedHashMap<>(); if (docs.size() <= 1) { - result.put("type", "yaml"); - result.put("data", docs.isEmpty() ? null : docs.getFirst()); + result.put(PROP_TYPE, "yaml"); + result.put(PROP_DATA, docs.isEmpty() ? null : docs.getFirst()); } else { - result.put("type", "yaml_multi"); + result.put(PROP_TYPE, "yaml_multi"); result.put("documents", docs); } return result; @@ -83,8 +86,8 @@ private Object parseYaml(String content) { private Object parseJson(String content) throws Exception { Object data = objectMapper.readValue(content, Object.class); Map result = new LinkedHashMap<>(); - result.put("type", "json"); - result.put("data", data); + result.put(PROP_TYPE, "json"); + result.put(PROP_DATA, data); return result; } @@ -107,7 +110,7 @@ private Object parseXml(String content, String filePath) throws Exception { var root = doc.getDocumentElement(); // Return a simple map with root element info for structured detectors Map result = new LinkedHashMap<>(); - result.put("type", "xml"); + result.put(PROP_TYPE, "xml"); result.put("file", filePath); result.put("rootElement", root.getTagName()); result.put("rootNamespace", root.getNamespaceURI()); @@ -150,8 +153,8 @@ private Object parseToml(String content) { } } Map result = new LinkedHashMap<>(); - result.put("type", "toml"); - result.put("data", data); + result.put(PROP_TYPE, "toml"); + result.put(PROP_DATA, data); return result; } @@ -180,8 +183,8 @@ private Object parseIni(String content) { } } Map result = new LinkedHashMap<>(); - result.put("type", "ini"); - result.put("data", data); + result.put(PROP_TYPE, "ini"); + result.put(PROP_DATA, data); return result; } @@ -195,8 +198,8 @@ private Object parseProperties(String content) throws Exception { data.put(key, props.getProperty(key)); } Map result = new LinkedHashMap<>(); - result.put("type", "properties"); - result.put("data", data); + result.put(PROP_TYPE, "properties"); + result.put(PROP_DATA, data); return result; } } diff --git a/src/main/java/io/github/randomcodespace/iq/cli/StatsCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/StatsCommand.java index 6576d8e3..933f2044 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/StatsCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/StatsCommand.java @@ -36,6 +36,15 @@ @Command(name = "stats", mixinStandardHelpOptions = true, description = "Show rich statistics from analyzed graph") public class StatsCommand implements Callable { + private static final String PROP_COUNT = "Count"; + private static final String PROP_ARCHITECTURE = "architecture"; + private static final String PROP_AUTH = "auth"; + private static final String PROP_CONNECTIONS = "connections"; + private static final String PROP_FRAMEWORKS = "frameworks"; + private static final String PROP_GRAPH = "graph"; + private static final String PROP_INFRA = "infra"; + private static final String PROP_LANGUAGES = "languages"; + @Parameters(index = "0", defaultValue = ".", description = "Path to analyzed codebase") private Path path; @@ -66,8 +75,8 @@ void setOut(PrintStream out) { private static final Set VALID_FORMATS = Set.of("pretty", "yaml", "json", "markdown"); private static final Set VALID_CATEGORIES = Set.of( - "all", "graph", "languages", "frameworks", "infra", - "connections", "auth", "architecture"); + "all", PROP_GRAPH, PROP_LANGUAGES, PROP_FRAMEWORKS, PROP_INFRA, + PROP_CONNECTIONS, PROP_AUTH, PROP_ARCHITECTURE); @Override public Integer call() { @@ -136,18 +145,18 @@ int outputPretty(Map stats) { out.println(); // Graph - if (stats.containsKey("graph")) { + if (stats.containsKey(PROP_GRAPH)) { @SuppressWarnings("unchecked") - Map graph = (Map) stats.get("graph"); + Map graph = (Map) stats.get(PROP_GRAPH); out.println(" Graph: " + nf.format(toLong(graph.get("nodes"))) + " nodes, " + nf.format(toLong(graph.get("edges"))) + " edges, " + nf.format(toLong(graph.get("files"))) + " files"); } // Languages - if (stats.containsKey("languages")) { + if (stats.containsKey(PROP_LANGUAGES)) { @SuppressWarnings("unchecked") - Map languages = (Map) stats.get("languages"); + Map languages = (Map) stats.get(PROP_LANGUAGES); if (!languages.isEmpty()) { StringBuilder sb = new StringBuilder(" Languages: "); languages.entrySet().stream().limit(10).forEach(e -> @@ -158,9 +167,9 @@ int outputPretty(Map stats) { } // Frameworks - if (stats.containsKey("frameworks")) { + if (stats.containsKey(PROP_FRAMEWORKS)) { @SuppressWarnings("unchecked") - Map frameworks = (Map) stats.get("frameworks"); + Map frameworks = (Map) stats.get(PROP_FRAMEWORKS); if (!frameworks.isEmpty()) { out.println(); StringBuilder sb = new StringBuilder(" Frameworks: "); @@ -174,7 +183,7 @@ int outputPretty(Map stats) { // Infrastructure if (stats.containsKey("infra")) { @SuppressWarnings("unchecked") - Map infra = (Map) stats.get("infra"); + Map infra = (Map) stats.get(PROP_INFRA); boolean hasInfra = infra.values().stream() .anyMatch(v -> v instanceof Map m && !m.isEmpty()); if (hasInfra) { @@ -187,9 +196,9 @@ int outputPretty(Map stats) { } // Connections - if (stats.containsKey("connections")) { + if (stats.containsKey(PROP_CONNECTIONS)) { @SuppressWarnings("unchecked") - Map conn = (Map) stats.get("connections"); + Map conn = (Map) stats.get(PROP_CONNECTIONS); out.println(); out.println(" Connections:"); @@ -220,7 +229,7 @@ int outputPretty(Map stats) { // Auth if (stats.containsKey("auth")) { @SuppressWarnings("unchecked") - Map auth = (Map) stats.get("auth"); + Map auth = (Map) stats.get(PROP_AUTH); if (!auth.isEmpty()) { out.println(); out.println(" Auth:"); @@ -230,9 +239,9 @@ int outputPretty(Map stats) { } // Architecture - if (stats.containsKey("architecture")) { + if (stats.containsKey(PROP_ARCHITECTURE)) { @SuppressWarnings("unchecked") - Map arch = (Map) stats.get("architecture"); + Map arch = (Map) stats.get(PROP_ARCHITECTURE); if (!arch.isEmpty()) { out.println(); out.println(" Architecture:"); @@ -274,7 +283,7 @@ int outputMarkdown(Map stats) { if (stats.containsKey("graph")) { @SuppressWarnings("unchecked") - Map graph = (Map) stats.get("graph"); + Map graph = (Map) stats.get(PROP_GRAPH); out.println("## Graph"); out.println(); out.println("| Metric | Count |"); @@ -283,12 +292,12 @@ int outputMarkdown(Map stats) { out.println(); } - printMarkdownSection(nf, stats, "languages", "Languages", "Language", "Count"); - printMarkdownSection(nf, stats, "frameworks", "Frameworks", "Framework", "Count"); + printMarkdownSection(nf, stats, PROP_LANGUAGES, "Languages", "Language", PROP_COUNT); + printMarkdownSection(nf, stats, PROP_FRAMEWORKS, "Frameworks", "Framework", PROP_COUNT); - if (stats.containsKey("infra")) { + if (stats.containsKey(PROP_INFRA)) { @SuppressWarnings("unchecked") - Map infra = (Map) stats.get("infra"); + Map infra = (Map) stats.get(PROP_INFRA); out.println("## Infrastructure"); out.println(); for (Map.Entry section : infra.entrySet()) { @@ -305,9 +314,9 @@ int outputMarkdown(Map stats) { } } - if (stats.containsKey("connections")) { + if (stats.containsKey(PROP_CONNECTIONS)) { @SuppressWarnings("unchecked") - Map conn = (Map) stats.get("connections"); + Map conn = (Map) stats.get(PROP_CONNECTIONS); out.println("## Connections"); out.println(); out.println("| Type | Count |"); @@ -330,8 +339,8 @@ int outputMarkdown(Map stats) { out.println(); } - printMarkdownSection(nf, stats, "auth", "Auth", "Type", "Count"); - printMarkdownSection(nf, stats, "architecture", "Architecture", "Kind", "Count"); + printMarkdownSection(nf, stats, PROP_AUTH, "Auth", "Type", PROP_COUNT); + printMarkdownSection(nf, stats, PROP_ARCHITECTURE, "Architecture", "Kind", PROP_COUNT); return 0; } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/auth/CertificateAuthDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/auth/CertificateAuthDetector.java index 69f302f4..5dd1616f 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/auth/CertificateAuthDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/auth/CertificateAuthDetector.java @@ -25,39 +25,44 @@ ) @Component public class CertificateAuthDetector extends AbstractRegexDetector { + private static final String PROP_AZURE_AD = "azure_ad"; + private static final String PROP_MTLS = "mtls"; + private static final String PROP_TLS_CONFIG = "tls_config"; + private static final String PROP_X509 = "x509"; + private record PatternDef(Pattern regex, String authType) {} private static final List MTLS_PATTERNS = List.of( - new PatternDef(Pattern.compile("\\bssl_verify_client\\b"), "mtls"), - new PatternDef(Pattern.compile("\\brequestCert\\s*:\\s*true\\b"), "mtls"), - new PatternDef(Pattern.compile("\\bclientAuth\\s*=\\s*\"true\""), "mtls"), - new PatternDef(Pattern.compile("\\bX509AuthenticationFilter\\b"), "mtls"), - new PatternDef(Pattern.compile("\\bAddCertificateForwarding\\b"), "mtls") + new PatternDef(Pattern.compile("\\bssl_verify_client\\b"), PROP_MTLS), + new PatternDef(Pattern.compile("\\brequestCert\\s*:\\s*true\\b"), PROP_MTLS), + new PatternDef(Pattern.compile("\\bclientAuth\\s*=\\s*\"true\""), PROP_MTLS), + new PatternDef(Pattern.compile("\\bX509AuthenticationFilter\\b"), PROP_MTLS), + new PatternDef(Pattern.compile("\\bAddCertificateForwarding\\b"), PROP_MTLS) ); private static final List X509_PATTERNS = List.of( - new PatternDef(Pattern.compile("\\bX509AuthenticationFilter\\b"), "x509"), - new PatternDef(Pattern.compile("\\bCertificateAuthenticationDefaults\\b"), "x509"), - new PatternDef(Pattern.compile("\\.x509\\s*\\("), "x509") + new PatternDef(Pattern.compile("\\bX509AuthenticationFilter\\b"), PROP_X509), + new PatternDef(Pattern.compile("\\bCertificateAuthenticationDefaults\\b"), PROP_X509), + new PatternDef(Pattern.compile("\\.x509\\s*\\("), PROP_X509) ); private static final List TLS_CONFIG_PATTERNS = List.of( - new PatternDef(Pattern.compile("\\bjavax\\.net\\.ssl\\.keyStore\\b"), "tls_config"), - new PatternDef(Pattern.compile("\\bssl\\.SSLContext\\b"), "tls_config"), - new PatternDef(Pattern.compile("\\btls\\.createServer\\b"), "tls_config"), - new PatternDef(Pattern.compile("(?:cert|key|ca)\\s*[=:]\\s*(?:fs\\.readFileSync\\s*\\(|['\"][\\w/.\\\\-]+\\.(?:pem|crt|key|cert)['\"])"), "tls_config"), - new PatternDef(Pattern.compile("\\btrustStore\\b"), "tls_config") + new PatternDef(Pattern.compile("\\bjavax\\.net\\.ssl\\.keyStore\\b"), PROP_TLS_CONFIG), + new PatternDef(Pattern.compile("\\bssl\\.SSLContext\\b"), PROP_TLS_CONFIG), + new PatternDef(Pattern.compile("\\btls\\.createServer\\b"), PROP_TLS_CONFIG), + new PatternDef(Pattern.compile("(?:cert|key|ca)\\s*[=:]\\s*(?:fs\\.readFileSync\\s*\\(|['\"][\\w/.\\\\-]+\\.(?:pem|crt|key|cert)['\"])"), PROP_TLS_CONFIG), + new PatternDef(Pattern.compile("\\btrustStore\\b"), PROP_TLS_CONFIG) ); private static final List AZURE_AD_PATTERNS = List.of( - new PatternDef(Pattern.compile("\\bAzureAd\\b"), "azure_ad"), - new PatternDef(Pattern.compile("\\bAZURE_TENANT_ID\\b"), "azure_ad"), - new PatternDef(Pattern.compile("\\bAZURE_CLIENT_ID\\b"), "azure_ad"), - new PatternDef(Pattern.compile("\\bmsal\\b"), "azure_ad"), - new PatternDef(Pattern.compile("['\"]@azure/msal-browser['\"]"), "azure_ad"), - new PatternDef(Pattern.compile("\\bAddMicrosoftIdentityWebApi\\b"), "azure_ad"), - new PatternDef(Pattern.compile("\\bClientCertificateCredential\\b"), "azure_ad") + new PatternDef(Pattern.compile("\\bAzureAd\\b"), PROP_AZURE_AD), + new PatternDef(Pattern.compile("\\bAZURE_TENANT_ID\\b"), PROP_AZURE_AD), + new PatternDef(Pattern.compile("\\bAZURE_CLIENT_ID\\b"), PROP_AZURE_AD), + new PatternDef(Pattern.compile("\\bmsal\\b"), PROP_AZURE_AD), + new PatternDef(Pattern.compile("['\"]@azure/msal-browser['\"]"), PROP_AZURE_AD), + new PatternDef(Pattern.compile("\\bAddMicrosoftIdentityWebApi\\b"), PROP_AZURE_AD), + new PatternDef(Pattern.compile("\\bClientCertificateCredential\\b"), PROP_AZURE_AD) ); private static final List ALL_PATTERNS; diff --git a/src/main/java/io/github/randomcodespace/iq/detector/auth/SessionHeaderAuthDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/auth/SessionHeaderAuthDetector.java index 9d57e597..6ffe5718 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/auth/SessionHeaderAuthDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/auth/SessionHeaderAuthDetector.java @@ -25,35 +25,40 @@ ) @Component public class SessionHeaderAuthDetector extends AbstractRegexDetector { + private static final String PROP_API_KEY = "api_key"; + private static final String PROP_CSRF = "csrf"; + private static final String PROP_HEADER = "header"; + private static final String PROP_SESSION = "session"; + private record PatternDef(Pattern regex, String authType, NodeKind nodeKind) {} private static final List SESSION_PATTERNS = List.of( - new PatternDef(Pattern.compile("['\"]express-session['\"]"), "session", NodeKind.MIDDLEWARE), - new PatternDef(Pattern.compile("['\"]cookie-session['\"]"), "session", NodeKind.MIDDLEWARE), - new PatternDef(Pattern.compile("@SessionAttributes\\b"), "session", NodeKind.GUARD), - new PatternDef(Pattern.compile("\\bSessionMiddleware\\b"), "session", NodeKind.MIDDLEWARE), - new PatternDef(Pattern.compile("\\bHttpSession\\b"), "session", NodeKind.GUARD), - new PatternDef(Pattern.compile("\\bSESSION_ENGINE\\b"), "session", NodeKind.GUARD) + new PatternDef(Pattern.compile("['\"]express-session['\"]"), PROP_SESSION, NodeKind.MIDDLEWARE), + new PatternDef(Pattern.compile("['\"]cookie-session['\"]"), PROP_SESSION, NodeKind.MIDDLEWARE), + new PatternDef(Pattern.compile("@SessionAttributes\\b"), PROP_SESSION, NodeKind.GUARD), + new PatternDef(Pattern.compile("\\bSessionMiddleware\\b"), PROP_SESSION, NodeKind.MIDDLEWARE), + new PatternDef(Pattern.compile("\\bHttpSession\\b"), PROP_SESSION, NodeKind.GUARD), + new PatternDef(Pattern.compile("\\bSESSION_ENGINE\\b"), PROP_SESSION, NodeKind.GUARD) ); private static final List HEADER_PATTERNS = List.of( - new PatternDef(Pattern.compile("['\"]X-API-Key['\"]", Pattern.CASE_INSENSITIVE), "header", NodeKind.GUARD), - new PatternDef(Pattern.compile("(?:req|request|ctx)\\.headers?\\s*\\[\\s*['\"]authorization['\"]\\s*]", Pattern.CASE_INSENSITIVE), "header", NodeKind.GUARD), - new PatternDef(Pattern.compile("getHeader\\s*\\(\\s*['\"]Authorization['\"]", Pattern.CASE_INSENSITIVE), "header", NodeKind.GUARD) + new PatternDef(Pattern.compile("['\"]X-API-Key['\"]", Pattern.CASE_INSENSITIVE), PROP_HEADER, NodeKind.GUARD), + new PatternDef(Pattern.compile("(?:req|request|ctx)\\.headers?\\s*\\[\\s*['\"]authorization['\"]\\s*]", Pattern.CASE_INSENSITIVE), PROP_HEADER, NodeKind.GUARD), + new PatternDef(Pattern.compile("getHeader\\s*\\(\\s*['\"]Authorization['\"]", Pattern.CASE_INSENSITIVE), PROP_HEADER, NodeKind.GUARD) ); private static final List API_KEY_PATTERNS = List.of( - new PatternDef(Pattern.compile("(?:req|request)\\.headers?\\s*\\[\\s*['\"]x-api-key['\"]\\s*]", Pattern.CASE_INSENSITIVE), "api_key", NodeKind.GUARD), - new PatternDef(Pattern.compile("\\bapi[_-]?key\\s*[=:]\\s*", Pattern.CASE_INSENSITIVE), "api_key", NodeKind.GUARD), - new PatternDef(Pattern.compile("\\bvalidate_?api_?key\\b", Pattern.CASE_INSENSITIVE), "api_key", NodeKind.GUARD) + new PatternDef(Pattern.compile("(?:req|request)\\.headers?\\s*\\[\\s*['\"]x-api-key['\"]\\s*]", Pattern.CASE_INSENSITIVE), PROP_API_KEY, NodeKind.GUARD), + new PatternDef(Pattern.compile("\\bapi[_-]?key\\s*[=:]\\s*", Pattern.CASE_INSENSITIVE), PROP_API_KEY, NodeKind.GUARD), + new PatternDef(Pattern.compile("\\bvalidate_?api_?key\\b", Pattern.CASE_INSENSITIVE), PROP_API_KEY, NodeKind.GUARD) ); private static final List CSRF_PATTERNS = List.of( - new PatternDef(Pattern.compile("@csrf_protect\\b"), "csrf", NodeKind.GUARD), - new PatternDef(Pattern.compile("\\bcsrf_exempt\\b"), "csrf", NodeKind.GUARD), - new PatternDef(Pattern.compile("\\bCsrfViewMiddleware\\b"), "csrf", NodeKind.MIDDLEWARE), - new PatternDef(Pattern.compile("['\"]csurf['\"]"), "csrf", NodeKind.MIDDLEWARE) + new PatternDef(Pattern.compile("@csrf_protect\\b"), PROP_CSRF, NodeKind.GUARD), + new PatternDef(Pattern.compile("\\bcsrf_exempt\\b"), PROP_CSRF, NodeKind.GUARD), + new PatternDef(Pattern.compile("\\bCsrfViewMiddleware\\b"), PROP_CSRF, NodeKind.MIDDLEWARE), + new PatternDef(Pattern.compile("['\"]csurf['\"]"), PROP_CSRF, NodeKind.MIDDLEWARE) ); private static final List ALL_PATTERNS; @@ -67,10 +72,10 @@ private record PatternDef(Pattern regex, String authType, NodeKind nodeKind) {} } private static final Map ID_TAG = Map.of( - "session", "session", - "header", "header", - "api_key", "apikey", - "csrf", "csrf" + PROP_SESSION, PROP_SESSION, + PROP_HEADER, PROP_HEADER, + PROP_API_KEY, "apikey", + PROP_CSRF, PROP_CSRF ); @Override diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/CloudFormationDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/CloudFormationDetector.java index acacd236..8b49c7b4 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/CloudFormationDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/CloudFormationDetector.java @@ -34,6 +34,8 @@ ) @Component public class CloudFormationDetector extends AbstractStructuredDetector { + private static final String PROP_TYPE = "Type"; + @Override public String getName() { @@ -66,7 +68,7 @@ public DetectorResult detect(DetectorContext ctx) { Map resource = asMap(entry.getValue()); if (resource.isEmpty()) continue; - String resourceType = getStringOrDefault(resource, "Type", "unknown"); + String resourceType = getStringOrDefault(resource, PROP_TYPE, "unknown"); String nodeId = "cfn:" + fp + ":resource:" + logicalId; Map props = new HashMap<>(); @@ -107,7 +109,7 @@ public DetectorResult detect(DetectorContext ctx) { Map paramDef = asMap(entry.getValue()); if (paramDef.isEmpty()) continue; - String paramType = getStringOrDefault(paramDef, "Type", "String"); + String paramType = getStringOrDefault(paramDef, PROP_TYPE, "String"); Map props = new HashMap<>(); props.put("param_type", paramType); props.put("cfn_type", "parameter"); @@ -179,7 +181,7 @@ private boolean isCfnTemplate(Map data) { Map resources = getMap(data, "Resources"); for (Object val : resources.values()) { Map resource = asMap(val); - String rtype = getString(resource, "Type"); + String rtype = getString(resource, PROP_TYPE); if (rtype != null && rtype.startsWith("AWS::")) return true; } return false; diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/GitLabCiDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/GitLabCiDetector.java index 7a5598b3..d040f9a6 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/GitLabCiDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/GitLabCiDetector.java @@ -33,10 +33,13 @@ ) @Component public class GitLabCiDetector extends AbstractStructuredDetector { + private static final String PROP_IMAGE = "image"; + private static final String PROP_STAGE = "stage"; + private static final Set GITLAB_CI_KEYWORDS = Set.of( "stages", "variables", "default", "workflow", "include", - "image", "services", "before_script", "after_script", "cache"); + PROP_IMAGE, "services", "before_script", "after_script", "cache"); private static final List TOOL_KEYWORDS = List.of( "docker", "helm", "kubectl", "terraform", "maven", "gradle", "npm", "pip"); @@ -88,7 +91,7 @@ public DetectorResult detect(DetectorContext ctx) { NodeKind.CONFIG_KEY, "stage:" + stageStr); stageNode.setModule(ctx.moduleName()); stageNode.setFilePath(fp); - stageNode.setProperties(Map.of("stage", stageStr)); + stageNode.setProperties(Map.of(PROP_STAGE, stageStr)); nodes.add(stageNode); } @@ -150,10 +153,10 @@ public DetectorResult detect(DetectorContext ctx) { String jobId = jobIds.get(jobName); Map props = new HashMap<>(); - String stageVal = getString(jobDef, "stage"); - if (stageVal != null) props.put("stage", stageVal); - String imageVal = getString(jobDef, "image"); - if (imageVal != null) props.put("image", imageVal); + String stageVal = getString(jobDef, PROP_STAGE); + if (stageVal != null) props.put(PROP_STAGE, stageVal); + String imageVal = getString(jobDef, PROP_IMAGE); + if (imageVal != null) props.put(PROP_IMAGE, imageVal); List scripts = getList(jobDef, "script"); List tools = detectTools(scripts); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/HelmChartDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/HelmChartDetector.java index d6608106..120d3055 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/HelmChartDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/HelmChartDetector.java @@ -35,6 +35,9 @@ ) @Component public class HelmChartDetector extends AbstractStructuredDetector { + private static final String PROP_TYPE = "type"; + private static final String PROP_VERSION = "version"; + private static final Pattern VALUES_REF_RE = Pattern.compile( "\\{\\{\\s*\\.Values\\.([a-zA-Z0-9_.]+)\\s*\\}\\}"); @@ -76,13 +79,13 @@ private void detectChartYaml(DetectorContext ctx, List nodes, List props = new HashMap<>(); props.put("chart_name", chartName); props.put("chart_version", chartVersion); - props.put("type", "helm_chart"); + props.put(PROP_TYPE, "helm_chart"); CodeNode chartNode = new CodeNode(chartNodeId, NodeKind.MODULE, "helm:" + chartName); chartNode.setFqn("helm:" + chartName + ":" + chartVersion); @@ -100,7 +103,7 @@ private void detectChartYaml(DetectorContext ctx, List nodes, List nodes, List nodes, List getYamlData(DetectorContext ctx) { if (parsedData == null) return null; Map pd = asMap(parsedData); - if (!"yaml".equals(getString(pd, "type"))) return null; + if (!"yaml".equals(getString(pd, PROP_TYPE))) return null; Map data = getMap(pd, "data"); return data.isEmpty() ? null : data; diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/IniStructureDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/IniStructureDetector.java index 82212c40..56fb93cf 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/IniStructureDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/IniStructureDetector.java @@ -19,7 +19,7 @@ /** * Detects INI file structures: sections, keys, and file identity. *

- * Expects parsedData to be a Map with type "ini" and "data" containing + * Expects parsedData to be a Map with type PROP_INI and "data" containing * a Map of section names to Maps of key-value pairs. */ @DetectorInfo( @@ -33,6 +33,8 @@ ) @Component public class IniStructureDetector extends AbstractStructuredDetector { + private static final String PROP_INI = "ini"; + @Override public String getName() { @@ -41,7 +43,7 @@ public String getName() { @Override public Set getSupportedLanguages() { - return Set.of("ini"); + return Set.of(PROP_INI); } @Override @@ -52,7 +54,7 @@ public DetectorResult detect(DetectorContext ctx) { List edges = new ArrayList<>(); // CONFIG_FILE node for the file itself - nodes.add(buildFileNode(ctx, "ini")); + nodes.add(buildFileNode(ctx, PROP_INI)); Object parsedData = ctx.parsedData(); if (parsedData == null) { diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/JsonStructureDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/JsonStructureDetector.java index 60d9f979..4d544edd 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/JsonStructureDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/JsonStructureDetector.java @@ -30,6 +30,8 @@ ) @Component public class JsonStructureDetector extends AbstractStructuredDetector { + private static final String PROP_JSON = "json"; + @Override public String getName() { @@ -38,7 +40,7 @@ public String getName() { @Override public Set getSupportedLanguages() { - return Set.of("json"); + return Set.of(PROP_JSON); } @Override @@ -49,7 +51,7 @@ public DetectorResult detect(DetectorContext ctx) { List edges = new ArrayList<>(); // CONFIG_FILE node for the file itself - nodes.add(buildFileNode(ctx, "json")); + nodes.add(buildFileNode(ctx, PROP_JSON)); // Extract data from parsed_data Object parsedData = ctx.parsedData(); @@ -65,7 +67,7 @@ public DetectorResult detect(DetectorContext ctx) { } for (String key : data.keySet()) { - addKeyNode(fileId, fp, key, "json", ctx, nodes, edges); + addKeyNode(fileId, fp, key, PROP_JSON, ctx, nodes, edges); } return DetectorResult.of(nodes, edges); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/KubernetesDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/KubernetesDetector.java index db226263..50714572 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/KubernetesDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/KubernetesDetector.java @@ -33,18 +33,32 @@ ) @Component public class KubernetesDetector extends AbstractStructuredDetector { + private static final String PROP_CRONJOB = "CronJob"; + private static final String PROP_DAEMONSET = "DaemonSet"; + private static final String PROP_DEPLOYMENT = "Deployment"; + private static final String PROP_POD = "Pod"; + private static final String PROP_STATEFULSET = "StatefulSet"; + private static final String PROP_DEFAULT = "default"; + private static final String PROP_KIND = "kind"; + private static final String PROP_LABELS = "labels"; + private static final String PROP_METADATA = "metadata"; + private static final String PROP_NAME = "name"; + private static final String PROP_NAMESPACE = "namespace"; + private static final String PROP_SELECTOR = "selector"; + private static final String PROP_SPEC = "spec"; + private static final Set K8S_KINDS = Set.of( - "Deployment", "Service", "ConfigMap", "Secret", "Ingress", - "Pod", "StatefulSet", "DaemonSet", "Job", "CronJob", + PROP_DEPLOYMENT, "Service", "ConfigMap", "Secret", "Ingress", + PROP_POD, PROP_STATEFULSET, PROP_DAEMONSET, "Job", PROP_CRONJOB, "Namespace", "PersistentVolumeClaim", "ServiceAccount", "Role", "RoleBinding", "ClusterRole", "ClusterRoleBinding"); private static final Set WORKLOAD_KINDS = Set.of( - "Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob", "Pod"); + PROP_DEPLOYMENT, PROP_STATEFULSET, PROP_DAEMONSET, "Job", PROP_CRONJOB, PROP_POD); private static final Set LABEL_TRACKING_KINDS = Set.of( - "Deployment", "StatefulSet", "DaemonSet"); + PROP_DEPLOYMENT, PROP_STATEFULSET, PROP_DAEMONSET); @Override public String getName() { @@ -73,20 +87,20 @@ public DetectorResult detect(DetectorContext ctx) { List ingressBackends = new ArrayList<>(); for (Map doc : documents) { - String kind = safeStr(doc.get("kind")); - Map metadata = asMap(doc.get("metadata")); - String name = safeStr(metadata.getOrDefault("name", "unknown")); - String namespace = safeStr(metadata.getOrDefault("namespace", "default")); - if (namespace.isEmpty()) namespace = "default"; + String kind = safeStr(doc.get(PROP_KIND)); + Map metadata = asMap(doc.get(PROP_METADATA)); + String name = safeStr(metadata.getOrDefault(PROP_NAME, "unknown")); + String namespace = safeStr(metadata.getOrDefault(PROP_NAMESPACE, PROP_DEFAULT)); + if (namespace.isEmpty()) namespace = PROP_DEFAULT; String nodeId = "k8s:" + fp + ":" + kind + ":" + namespace + "/" + name; Map props = new HashMap<>(); - props.put("kind", kind); - props.put("namespace", namespace); - Object labels = metadata.get("labels"); + props.put(PROP_KIND, kind); + props.put(PROP_NAMESPACE, namespace); + Object labels = metadata.get(PROP_LABELS); if (labels instanceof Map) { - props.put("labels", labels); + props.put(PROP_LABELS, labels); } Object annotations = metadata.get("annotations"); if (annotations instanceof Map) { @@ -101,13 +115,13 @@ public DetectorResult detect(DetectorContext ctx) { resourceNode.setProperties(props); nodes.add(resourceNode); - Map spec = asMap(doc.get("spec")); + Map spec = asMap(doc.get(PROP_SPEC)); // Extract container specs from workload resources if (WORKLOAD_KINDS.contains(kind)) { List> containers = extractContainers(spec, kind); for (Map container : containers) { - String cName = safeStr(container.getOrDefault("name", "unnamed")); + String cName = safeStr(container.getOrDefault(PROP_NAME, "unnamed")); Map cProps = new HashMap<>(); String image = getString(container, "image"); @@ -135,7 +149,7 @@ public DetectorResult detect(DetectorContext ctx) { List envNames = new ArrayList<>(); for (Object e : envVars) { Map em = asMap(e); - String envName = getString(em, "name"); + String envName = getString(em, PROP_NAME); if (envName != null) { envNames.add(envName); } @@ -157,13 +171,13 @@ public DetectorResult detect(DetectorContext ctx) { // Track deployment match labels if (LABEL_TRACKING_KINDS.contains(kind)) { Map template = getMap(spec, "template"); - Map tmplMeta = getMap(template, "metadata"); - Map tmplLabels = getMap(tmplMeta, "labels"); + Map tmplMeta = getMap(template, PROP_METADATA); + Map tmplLabels = getMap(tmplMeta, PROP_LABELS); for (var le : tmplLabels.entrySet()) { deploymentLabels.put(le.getKey() + "=" + le.getValue(), nodeId); } - Map selector = getMap(spec, "selector"); + Map selector = getMap(spec, PROP_SELECTOR); Map matchLabels = getMap(selector, "matchLabels"); for (var le : matchLabels.entrySet()) { deploymentLabels.put(le.getKey() + "=" + le.getValue(), nodeId); @@ -172,7 +186,7 @@ public DetectorResult detect(DetectorContext ctx) { // Track service selectors if ("Service".equals(kind)) { - Map svcSelector = getMap(spec, "selector"); + Map svcSelector = getMap(spec, PROP_SELECTOR); if (!svcSelector.isEmpty()) { serviceSelectors.add(new SelectorEntry(nodeId, svcSelector)); } @@ -191,7 +205,7 @@ public DetectorResult detect(DetectorContext ctx) { String targetId = deploymentLabels.get(labelTag); if (targetId != null) { edges.add(createEdge(se.nodeId, targetId, EdgeKind.DEPENDS_ON, - "service selects " + labelTag, Map.of("selector", labelTag))); + "service selects " + labelTag, Map.of(PROP_SELECTOR, labelTag))); } } } @@ -199,11 +213,11 @@ public DetectorResult detect(DetectorContext ctx) { // Resolve ingress -> service edges Map serviceNameToId = new LinkedHashMap<>(); for (Map doc : documents) { - if (!"Service".equals(doc.get("kind"))) continue; - Map meta = asMap(doc.get("metadata")); - String svcName = safeStr(meta.getOrDefault("name", "")); - String ns = safeStr(meta.getOrDefault("namespace", "default")); - if (ns.isEmpty()) ns = "default"; + if (!"Service".equals(doc.get(PROP_KIND))) continue; + Map meta = asMap(doc.get(PROP_METADATA)); + String svcName = safeStr(meta.getOrDefault(PROP_NAME, "")); + String ns = safeStr(meta.getOrDefault(PROP_NAMESPACE, PROP_DEFAULT)); + if (ns.isEmpty()) ns = PROP_DEFAULT; serviceNameToId.put(svcName, "k8s:" + fp + ":Service:" + ns + "/" + svcName); } @@ -230,7 +244,7 @@ private List> getDocuments(DetectorContext ctx) { List> result = new ArrayList<>(); for (Object d : docs) { Map doc = asMap(d); - String docKind = getString(doc, "kind"); + String docKind = getString(doc, PROP_KIND); if (docKind != null && K8S_KINDS.contains(docKind)) { result.add(doc); } @@ -240,7 +254,7 @@ private List> getDocuments(DetectorContext ctx) { if ("yaml".equals(ptype)) { Map data = getMap(pd, "data"); - String dataKind = getString(data, "kind"); + String dataKind = getString(data, PROP_KIND); if (dataKind != null && K8S_KINDS.contains(dataKind)) { return List.of(data); } @@ -265,12 +279,12 @@ private List> extractContainers(Map spec, St Map workSpec = spec; if ("CronJob".equals(kind)) { Map jobTemplate = getMap(spec, "jobTemplate"); - workSpec = getMap(jobTemplate, "spec"); + workSpec = getMap(jobTemplate, PROP_SPEC); if (workSpec.isEmpty()) return containers; } Map template = getMap(workSpec, "template"); - Map podSpec = getMap(template, "spec"); + Map podSpec = getMap(template, PROP_SPEC); List cs = getList(podSpec, "containers"); for (Object c : cs) { @@ -296,7 +310,7 @@ private void collectIngressBackends(Map spec, String ingressNode if (!defaultBackend.isEmpty()) { Map svc = getMap(defaultBackend, "service"); if (svc.isEmpty()) svc = defaultBackend; - String svcName = getString(svc, "name"); + String svcName = getString(svc, PROP_NAME); if (svcName == null) svcName = getString(svc, "serviceName"); if (svcName != null) { out.add(new IngressBackend(ingressNodeId, svcName)); @@ -315,7 +329,7 @@ private void collectIngressBackends(Map spec, String ingressNode if (backend.isEmpty()) continue; Map svc = getMap(backend, "service"); if (svc.isEmpty()) svc = backend; - String svcName = getString(svc, "name"); + String svcName = getString(svc, PROP_NAME); if (svcName == null) svcName = getString(svc, "serviceName"); if (svcName != null) { out.add(new IngressBackend(ingressNodeId, svcName)); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/KubernetesRbacDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/KubernetesRbacDetector.java index aef31d03..b5131125 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/KubernetesRbacDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/KubernetesRbacDetector.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Component; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -33,9 +32,20 @@ ) @Component public class KubernetesRbacDetector extends AbstractStructuredDetector { + private static final String PROP_CLUSTERROLE = "ClusterRole"; + private static final String PROP_SERVICEACCOUNT = "ServiceAccount"; + private static final String PROP_AUTH_TYPE = "auth_type"; + private static final String PROP_DEFAULT = "default"; + private static final String PROP_K8S_KIND = "k8s_kind"; + private static final String PROP_K8S_RBAC = "k8s_rbac"; + private static final String PROP_KIND = "kind"; + private static final String PROP_NAME = "name"; + private static final String PROP_NAMESPACE = "namespace"; + private static final String PROP_RULES = "rules"; + private static final Set RBAC_KINDS = Set.of( - "Role", "ClusterRole", "RoleBinding", "ClusterRoleBinding", "ServiceAccount"); + "Role", PROP_CLUSTERROLE, "RoleBinding", "ClusterRoleBinding", PROP_SERVICEACCOUNT); @Override public String getName() { @@ -63,16 +73,16 @@ public DetectorResult detect(DetectorContext ctx) { List> bindings = new ArrayList<>(); for (Map doc : documents) { - String kind = safeStr(doc.get("kind")); + String kind = safeStr(doc.get(PROP_KIND)); Map metadata = asMap(doc.get("metadata")); - String name = safeStr(metadata.getOrDefault("name", "unknown")); - String namespace = safeStr(metadata.getOrDefault("namespace", "default")); - if (namespace.isEmpty()) namespace = "default"; + String name = safeStr(metadata.getOrDefault(PROP_NAME, "unknown")); + String namespace = safeStr(metadata.getOrDefault(PROP_NAMESPACE, PROP_DEFAULT)); + if (namespace.isEmpty()) namespace = PROP_DEFAULT; String nodeId = "k8s_rbac:" + fp + ":" + kind + ":" + namespace + "/" + name; if ("Role".equals(kind) || "ClusterRole".equals(kind)) { - List rules = getList(doc, "rules"); + List rules = getList(doc, PROP_RULES); List> serializedRules = new ArrayList<>(); for (Object rule : rules) { Map rm = asMap(rule); @@ -86,10 +96,10 @@ public DetectorResult detect(DetectorContext ctx) { } Map props = new LinkedHashMap<>(); - props.put("auth_type", "k8s_rbac"); - props.put("k8s_kind", kind); - props.put("namespace", namespace); - props.put("rules", serializedRules); + props.put(PROP_AUTH_TYPE, PROP_K8S_RBAC); + props.put(PROP_K8S_KIND, kind); + props.put(PROP_NAMESPACE, namespace); + props.put(PROP_RULES, serializedRules); CodeNode node = new CodeNode(nodeId, NodeKind.GUARD, kind + "/" + name); node.setFqn("k8s:" + kind + ":" + namespace + "/" + name); @@ -105,10 +115,10 @@ public DetectorResult detect(DetectorContext ctx) { } else if ("ServiceAccount".equals(kind)) { Map props = new LinkedHashMap<>(); - props.put("auth_type", "k8s_rbac"); - props.put("k8s_kind", "ServiceAccount"); - props.put("namespace", namespace); - props.put("rules", List.of()); + props.put(PROP_AUTH_TYPE, PROP_K8S_RBAC); + props.put(PROP_K8S_KIND, PROP_SERVICEACCOUNT); + props.put(PROP_NAMESPACE, namespace); + props.put(PROP_RULES, List.of()); CodeNode node = new CodeNode(nodeId, NodeKind.GUARD, "ServiceAccount/" + name); @@ -122,10 +132,10 @@ public DetectorResult detect(DetectorContext ctx) { } else if ("RoleBinding".equals(kind) || "ClusterRoleBinding".equals(kind)) { Map props = new LinkedHashMap<>(); - props.put("auth_type", "k8s_rbac"); - props.put("k8s_kind", kind); - props.put("namespace", namespace); - props.put("rules", List.of()); + props.put(PROP_AUTH_TYPE, PROP_K8S_RBAC); + props.put(PROP_K8S_KIND, kind); + props.put(PROP_NAMESPACE, namespace); + props.put(PROP_RULES, List.of()); CodeNode node = new CodeNode(nodeId, NodeKind.GUARD, kind + "/" + name); node.setFqn("k8s:" + kind + ":" + namespace + "/" + name); @@ -140,16 +150,16 @@ public DetectorResult detect(DetectorContext ctx) { // Resolve RoleBinding/ClusterRoleBinding -> PROTECTS edges for (Map doc : bindings) { - String kind = safeStr(doc.get("kind")); + String kind = safeStr(doc.get(PROP_KIND)); Map metadata = asMap(doc.get("metadata")); - String bindingNamespace = safeStr(metadata.getOrDefault("namespace", "default")); - if (bindingNamespace.isEmpty()) bindingNamespace = "default"; + String bindingNamespace = safeStr(metadata.getOrDefault(PROP_NAMESPACE, PROP_DEFAULT)); + if (bindingNamespace.isEmpty()) bindingNamespace = PROP_DEFAULT; Map roleRef = getMap(doc, "roleRef"); if (roleRef.isEmpty()) continue; - String refKind = safeStr(roleRef.get("kind")); - String refName = safeStr(roleRef.get("name")); + String refKind = safeStr(roleRef.get(PROP_KIND)); + String refName = safeStr(roleRef.get(PROP_NAME)); String roleKey = "ClusterRole".equals(refKind) ? "ClusterRole:cluster-wide/" + refName @@ -163,9 +173,9 @@ public DetectorResult detect(DetectorContext ctx) { Map subj = asMap(subject); if (subj.isEmpty()) continue; - String subjKind = safeStr(subj.get("kind")); - String subjName = safeStr(subj.get("name")); - String subjNamespace = safeStr(subj.getOrDefault("namespace", bindingNamespace)); + String subjKind = safeStr(subj.get(PROP_KIND)); + String subjName = safeStr(subj.get(PROP_NAME)); + String subjNamespace = safeStr(subj.getOrDefault(PROP_NAMESPACE, bindingNamespace)); if (subjNamespace.isEmpty()) subjNamespace = bindingNamespace; if ("ServiceAccount".equals(subjKind)) { @@ -199,7 +209,7 @@ private List> getDocuments(DetectorContext ctx) { List> result = new ArrayList<>(); for (Object d : docs) { Map doc = asMap(d); - String docKind = getString(doc, "kind"); + String docKind = getString(doc, PROP_KIND); if (docKind != null && RBAC_KINDS.contains(docKind)) { result.add(doc); } @@ -209,7 +219,7 @@ private List> getDocuments(DetectorContext ctx) { if ("yaml".equals(ptype)) { Map data = getMap(pd, "data"); - String dataKind = getString(data, "kind"); + String dataKind = getString(data, PROP_KIND); if (dataKind != null && RBAC_KINDS.contains(dataKind)) { return List.of(data); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/OpenApiDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/OpenApiDetector.java index d5132921..3e8970a3 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/OpenApiDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/OpenApiDetector.java @@ -33,13 +33,15 @@ ) @Component public class OpenApiDetector extends AbstractStructuredDetector { + private static final String PROP_OPENAPI = "openapi"; + private static final Set HTTP_METHODS = Set.of( "get", "post", "put", "patch", "delete", "head", "options", "trace"); @Override public String getName() { - return "openapi"; + return PROP_OPENAPI; } @Override @@ -57,7 +59,7 @@ public DetectorResult detect(DetectorContext ctx) { if (spec.isEmpty()) return DetectorResult.empty(); // Only trigger for OpenAPI or Swagger spec - if (!spec.containsKey("openapi") && !spec.containsKey("swagger")) { + if (!spec.containsKey(PROP_OPENAPI) && !spec.containsKey("swagger")) { return DetectorResult.empty(); } @@ -71,13 +73,13 @@ public DetectorResult detect(DetectorContext ctx) { String apiTitle = getString(info, "title"); if (apiTitle == null) apiTitle = filepath; String apiVersion = getStringOrDefault(info, "version", ""); - Object specVersionObj = spec.get("openapi"); + Object specVersionObj = spec.get(PROP_OPENAPI); if (specVersionObj == null) specVersionObj = spec.get("swagger"); String specVersion = specVersionObj != null ? String.valueOf(specVersionObj) : ""; // CONFIG_FILE node for the spec Map cfProps = new HashMap<>(); - cfProps.put("config_type", "openapi"); + cfProps.put("config_type", PROP_OPENAPI); cfProps.put("api_title", apiTitle); cfProps.put("api_version", apiVersion); cfProps.put("spec_version", specVersion); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/PropertiesDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/PropertiesDetector.java index 44556db5..31e34d90 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/PropertiesDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/PropertiesDetector.java @@ -34,6 +34,8 @@ ) @Component public class PropertiesDetector extends AbstractStructuredDetector { + private static final String PROP_PROPERTIES = "properties"; + private static final Set DB_URL_KEYWORDS = Set.of("url", "jdbc-url", "uri"); private static final Pattern JDBC_DB_TYPE_RE = Pattern.compile( @@ -54,12 +56,12 @@ public class PropertiesDetector extends AbstractStructuredDetector { @Override public String getName() { - return "properties"; + return PROP_PROPERTIES; } @Override public Set getSupportedLanguages() { - return Set.of("properties"); + return Set.of(PROP_PROPERTIES); } @Override @@ -86,7 +88,7 @@ public DetectorResult detect(DetectorContext ctx) { fileNode.setModule(ctx.moduleName()); fileNode.setFilePath(filepath); fileNode.setLineStart(1); - fileNode.setProperties(Map.of("format", "properties")); + fileNode.setProperties(Map.of("format", PROP_PROPERTIES)); nodes.add(fileNode); // Process keys (limit to avoid node explosion) diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/PyprojectTomlDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/PyprojectTomlDetector.java index e52d6ba4..b2368d96 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/PyprojectTomlDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/PyprojectTomlDetector.java @@ -35,6 +35,9 @@ ) @Component public class PyprojectTomlDetector extends AbstractStructuredDetector { + private static final String PROP_DESCRIPTION = "description"; + private static final String PROP_VERSION = "version"; + @Override public String getName() { @@ -76,12 +79,12 @@ public DetectorResult detect(DetectorContext ctx) { Map props = new HashMap<>(); props.put("package_name", pkgName); - String version = getString(projectSection, "version"); - if (version == null) version = getString(poetrySection, "version"); - if (version != null) props.put("version", version); - String description = getString(projectSection, "description"); - if (description == null) description = getString(poetrySection, "description"); - if (description != null) props.put("description", description); + String version = getString(projectSection, PROP_VERSION); + if (version == null) version = getString(poetrySection, PROP_VERSION); + if (version != null) props.put(PROP_VERSION, version); + String description = getString(projectSection, PROP_DESCRIPTION); + if (description == null) description = getString(poetrySection, PROP_DESCRIPTION); + if (description != null) props.put(PROP_DESCRIPTION, description); CodeNode moduleNode = new CodeNode(moduleId, NodeKind.MODULE, pkgName); moduleNode.setFqn(pkgName); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/SqlStructureDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/SqlStructureDetector.java index 452c1baa..f6c4da59 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/SqlStructureDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/SqlStructureDetector.java @@ -31,6 +31,8 @@ ) @Component public class SqlStructureDetector extends AbstractRegexDetector { + private static final String PROP_ENTITY_TYPE = "entity_type"; + private static final Pattern TABLE_RE = Pattern.compile( "CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:\\w+\\.)?(\\w+)", @@ -67,7 +69,6 @@ public DetectorResult detect(DetectorContext ctx) { List nodes = new ArrayList<>(); List edges = new ArrayList<>(); - String currentTable = null; String currentTableId = null; for (IndexedLine il : iterLines(content)) { @@ -79,7 +80,6 @@ public DetectorResult detect(DetectorContext ctx) { m = TABLE_RE.matcher(line); if (m.find()) { String tableName = m.group(1); - currentTable = tableName; currentTableId = "sql:" + filepath + ":table:" + tableName; CodeNode node = new CodeNode(currentTableId, NodeKind.ENTITY, tableName); @@ -87,7 +87,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setModule(ctx.moduleName()); node.setFilePath(filepath); node.setLineStart(lineNum); - node.setProperties(Map.of("entity_type", "table")); + node.setProperties(Map.of(PROP_ENTITY_TYPE, "table")); nodes.add(node); continue; } @@ -102,9 +102,8 @@ public DetectorResult detect(DetectorContext ctx) { node.setModule(ctx.moduleName()); node.setFilePath(filepath); node.setLineStart(lineNum); - node.setProperties(Map.of("entity_type", "view")); + node.setProperties(Map.of(PROP_ENTITY_TYPE, "view")); nodes.add(node); - currentTable = null; currentTableId = null; continue; } @@ -134,9 +133,8 @@ public DetectorResult detect(DetectorContext ctx) { node.setModule(ctx.moduleName()); node.setFilePath(filepath); node.setLineStart(lineNum); - node.setProperties(Map.of("entity_type", "procedure")); + node.setProperties(Map.of(PROP_ENTITY_TYPE, "procedure")); nodes.add(node); - currentTable = null; currentTableId = null; continue; } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/TomlStructureDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/TomlStructureDetector.java index fed52902..9e6446fe 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/TomlStructureDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/TomlStructureDetector.java @@ -21,7 +21,7 @@ /** * Detects TOML file structures: sections, top-level keys, and file identity. *

- * Expects parsedData to be a Map with type "toml" and "data" containing the parsed structure. + * Expects parsedData to be a Map with type PROP_TOML and "data" containing the parsed structure. */ @DetectorInfo( name = "toml_structure", @@ -34,6 +34,8 @@ ) @Component public class TomlStructureDetector extends AbstractStructuredDetector { + private static final String PROP_TOML = "toml"; + @Override public String getName() { @@ -42,7 +44,7 @@ public String getName() { @Override public Set getSupportedLanguages() { - return Set.of("toml"); + return Set.of(PROP_TOML); } @Override @@ -53,7 +55,7 @@ public DetectorResult detect(DetectorContext ctx) { List edges = new ArrayList<>(); // CONFIG_FILE node for the file itself - nodes.add(buildFileNode(ctx, "toml")); + nodes.add(buildFileNode(ctx, PROP_TOML)); Object parsedData = ctx.parsedData(); if (parsedData == null) { diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/TsconfigJsonDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/TsconfigJsonDetector.java index 4e6ace24..b6972d51 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/TsconfigJsonDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/TsconfigJsonDetector.java @@ -33,6 +33,8 @@ ) @Component public class TsconfigJsonDetector extends AbstractStructuredDetector { + private static final String PROP_EXTENDS = "extends"; + private static final Pattern TSCONFIG_RE = Pattern.compile("^tsconfig(?:\\..+)?\\.json$"); private static final List TRACKED_COMPILER_OPTIONS = List.of( @@ -75,15 +77,15 @@ public DetectorResult detect(DetectorContext ctx) { configNode.setProperties(Map.of("config_type", "tsconfig")); nodes.add(configNode); - // DEPENDS_ON edge for "extends" - String extendsVal = getString(cfg, "extends"); + // DEPENDS_ON edge for PROP_EXTENDS + String extendsVal = getString(cfg, PROP_EXTENDS); if (extendsVal != null && !extendsVal.isEmpty()) { CodeEdge edge = new CodeEdge(); edge.setId(configId + "->" + extendsVal); edge.setKind(EdgeKind.DEPENDS_ON); edge.setSourceId(configId); edge.setTarget(new CodeNode(extendsVal, null, null)); - edge.setProperties(Map.of("relation", "extends")); + edge.setProperties(Map.of("relation", PROP_EXTENDS)); edges.add(edge); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/config/YamlStructureDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/config/YamlStructureDetector.java index 4b529872..0b0b8e1b 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/config/YamlStructureDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/config/YamlStructureDetector.java @@ -31,6 +31,8 @@ ) @Component public class YamlStructureDetector extends AbstractStructuredDetector { + private static final String PROP_YAML = "yaml"; + @Override public String getName() { @@ -39,7 +41,7 @@ public String getName() { @Override public Set getSupportedLanguages() { - return Set.of("yaml"); + return Set.of(PROP_YAML); } @Override @@ -50,7 +52,7 @@ public DetectorResult detect(DetectorContext ctx) { List edges = new ArrayList<>(); // CONFIG_FILE node for the file itself - nodes.add(buildFileNode(ctx, "yaml")); + nodes.add(buildFileNode(ctx, PROP_YAML)); Object parsedData = ctx.parsedData(); if (parsedData == null) { @@ -78,7 +80,7 @@ public DetectorResult detect(DetectorContext ctx) { } for (String keyStr : topLevelKeys) { - addKeyNode(fileId, fp, keyStr, "yaml", ctx, nodes, edges); + addKeyNode(fileId, fp, keyStr, PROP_YAML, ctx, nodes, edges); } return DetectorResult.of(nodes, edges); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/csharp/CSharpEfcoreDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/csharp/CSharpEfcoreDetector.java index 36fb55f6..f30261d0 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/csharp/CSharpEfcoreDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/csharp/CSharpEfcoreDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.csharp; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -30,6 +29,9 @@ ) @Component public class CSharpEfcoreDetector extends AbstractAntlrDetector { + private static final String PROP_EFCORE = "efcore"; + private static final String PROP_FRAMEWORK = "framework"; + private static final Pattern DBCONTEXT_RE = Pattern.compile("class\\s+(\\w+)\\s*:\\s*(?:[\\w.]+\\.)?DbContext", Pattern.MULTILINE); private static final Pattern DBSET_RE = Pattern.compile("DbSet<(\\w+)>", Pattern.MULTILINE); @@ -71,7 +73,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(contextName); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, m.start())); - node.getProperties().put("framework", "efcore"); + node.getProperties().put(PROP_FRAMEWORK, PROP_EFCORE); nodes.add(node); } @@ -87,7 +89,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(entityName); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, m.start())); - node.getProperties().put("framework", "efcore"); + node.getProperties().put(PROP_FRAMEWORK, PROP_EFCORE); nodes.add(node); for (String ctxId : contextIds) { @@ -111,7 +113,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(migrationName); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, m.start())); - node.getProperties().put("framework", "efcore"); + node.getProperties().put(PROP_FRAMEWORK, PROP_EFCORE); nodes.add(node); } @@ -129,7 +131,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(tableName); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, m.start())); - node.getProperties().put("framework", "efcore"); + node.getProperties().put(PROP_FRAMEWORK, PROP_EFCORE); node.getProperties().put("source", "migration"); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/csharp/CSharpMinimalApisDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/csharp/CSharpMinimalApisDetector.java index 1eecfc69..50eb5fbc 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/csharp/CSharpMinimalApisDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/csharp/CSharpMinimalApisDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.csharp; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -30,6 +29,9 @@ ) @Component public class CSharpMinimalApisDetector extends AbstractAntlrDetector { + private static final String PROP_DOTNET_MINIMAL_API = "dotnet_minimal_api"; + private static final String PROP_FRAMEWORK = "framework"; + private static final Pattern MAP_RE = Pattern.compile("\\.Map(Get|Post|Put|Delete|Patch)\\s*\\(\\s*\"([^\"]*)\"", Pattern.MULTILINE); private static final Pattern BUILDER_RE = Pattern.compile("WebApplication\\.CreateBuilder\\s*\\(", Pattern.MULTILINE); @@ -68,7 +70,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(filePath); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, bm.start())); - node.getProperties().put("framework", "dotnet_minimal_api"); + node.getProperties().put(PROP_FRAMEWORK, PROP_DOTNET_MINIMAL_API); nodes.add(node); } @@ -87,7 +89,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setLineStart(line); node.getProperties().put("http_method", httpMethod); node.getProperties().put("path", path); - node.getProperties().put("framework", "dotnet_minimal_api"); + node.getProperties().put(PROP_FRAMEWORK, PROP_DOTNET_MINIMAL_API); nodes.add(node); if (appModuleId != null) { @@ -114,7 +116,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFilePath(filePath); node.setLineStart(line); node.getProperties().put("guard_type", authType.toLowerCase()); - node.getProperties().put("framework", "dotnet_minimal_api"); + node.getProperties().put(PROP_FRAMEWORK, PROP_DOTNET_MINIMAL_API); nodes.add(node); } } 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 9160d371..2fd61ac9 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 @@ -22,6 +22,10 @@ ) @Component public class AngularComponentDetector extends AbstractRegexDetector { + private static final String PROP_ANGULAR = "angular"; + private static final String PROP_COMPONENT = "component"; + private static final String PROP_DECORATOR = "decorator"; + private static final Pattern COMPONENT_DECORATOR = Pattern.compile( "@Component\\s*\\(\\s*\\{.*?selector\\s*:\\s*['\"]([^'\"]+)['\"].*?\\}\\s*\\)\\s*\\n?\\s*(?:export\\s+)?class\\s+(\\w+)", @@ -71,10 +75,10 @@ public DetectorResult detect(DetectorContext ctx) { String selector = m.group(1); String className = m.group(2); if (!seen.add(className)) continue; - CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_ANGULAR, filePath, PROP_COMPONENT, className, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("selector", selector); - node.getProperties().put("decorator", "Component"); + node.getProperties().put(PROP_DECORATOR, "Component"); nodes.add(node); } @@ -84,10 +88,10 @@ public DetectorResult detect(DetectorContext ctx) { String providedIn = m.group(1); String className = m.group(2); if (!seen.add(className)) continue; - CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "service", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_ANGULAR, filePath, "service", className, NodeKind.MIDDLEWARE, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("provided_in", providedIn); - node.getProperties().put("decorator", "Injectable"); + node.getProperties().put(PROP_DECORATOR, "Injectable"); nodes.add(node); } @@ -97,10 +101,10 @@ public DetectorResult detect(DetectorContext ctx) { String selector = m.group(1); String className = m.group(2); if (!seen.add(className)) continue; - CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_ANGULAR, filePath, PROP_COMPONENT, className, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("selector", selector); - node.getProperties().put("decorator", "Directive"); + node.getProperties().put(PROP_DECORATOR, "Directive"); nodes.add(node); } @@ -110,10 +114,10 @@ public DetectorResult detect(DetectorContext ctx) { String pipeName = m.group(1); String className = m.group(2); if (!seen.add(className)) continue; - CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_ANGULAR, filePath, PROP_COMPONENT, className, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("pipe_name", pipeName); - node.getProperties().put("decorator", "Pipe"); + node.getProperties().put(PROP_DECORATOR, "Pipe"); nodes.add(node); } @@ -122,9 +126,9 @@ public DetectorResult detect(DetectorContext ctx) { while (m.find()) { String className = m.group(1); if (!seen.add(className)) continue; - CodeNode node = FrontendDetectorHelper.createComponentNode("angular", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_ANGULAR, filePath, PROP_COMPONENT, className, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); - node.getProperties().put("decorator", "NgModule"); + node.getProperties().put(PROP_DECORATOR, "NgModule"); nodes.add(node); } 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 bd87ddd8..d346483f 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 @@ -25,6 +25,8 @@ ) @Component public class ReactComponentDetector extends AbstractRegexDetector { + private static final String PROP_REACT = "react"; + private static final Pattern EXPORT_DEFAULT_FUNC = Pattern.compile("export\\s+default\\s+function\\s+([A-Z]\\w*)\\s*\\("); private static final Pattern EXPORT_CONST_ARROW = Pattern.compile("export\\s+const\\s+([A-Z]\\w*)\\s*=\\s*\\("); @@ -68,7 +70,7 @@ record ComponentEntry(String name, String sourceId, int matchStart) {} String name = m.group(1); if (componentNames.contains(name)) continue; String sourceId = "react:" + filePath + ":component:" + name; - CodeNode node = FrontendDetectorHelper.createComponentNode("react", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_REACT, filePath, "component", name, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("component_type", "function"); nodes.add(node); @@ -84,7 +86,7 @@ record ComponentEntry(String name, String sourceId, int matchStart) {} String name = m.group(1); if (componentNames.contains(name)) continue; String sourceId = "react:" + filePath + ":component:" + name; - CodeNode node = FrontendDetectorHelper.createComponentNode("react", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_REACT, filePath, "component", name, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); node.getProperties().put("component_type", "class"); nodes.add(node); @@ -100,7 +102,7 @@ record ComponentEntry(String name, String sourceId, int matchStart) {} while (m.find()) { String name = m.group(1); if (hookNames.contains(name)) continue; - nodes.add(FrontendDetectorHelper.createComponentNode("react", filePath, "hook", + nodes.add(FrontendDetectorHelper.createComponentNode(PROP_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 8c8d85c0..70a34a74 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 @@ -24,6 +24,10 @@ ) @Component public class VueComponentDetector extends AbstractRegexDetector { + private static final String PROP_API_STYLE = "api_style"; + private static final String PROP_COMPONENT = "component"; + private static final String PROP_VUE = "vue"; + private static final Pattern DEFINE_COMPONENT_NAME = Pattern.compile( "export\\s+default\\s+defineComponent\\s*\\(\\s*\\{[^}]*?name\\s*:\\s*['\"]([\\w]+)['\"]", @@ -49,7 +53,7 @@ public String getName() { @Override public Set getSupportedLanguages() { - return Set.of("typescript", "javascript", "vue"); + return Set.of("typescript", "javascript", PROP_VUE); } @Override @@ -68,9 +72,9 @@ public DetectorResult detect(DetectorContext ctx) { while (m.find()) { String name = m.group(1); if (componentNames.contains(name)) continue; - CodeNode node = FrontendDetectorHelper.createComponentNode("vue", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_VUE, filePath, PROP_COMPONENT, name, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); - node.getProperties().put("api_style", "composition"); + node.getProperties().put(PROP_API_STYLE, "composition"); nodes.add(node); componentNames.add(name); } @@ -80,9 +84,9 @@ public DetectorResult detect(DetectorContext ctx) { while (m.find()) { String name = m.group(1); if (componentNames.contains(name)) continue; - CodeNode node = FrontendDetectorHelper.createComponentNode("vue", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_VUE, filePath, PROP_COMPONENT, name, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); - node.getProperties().put("api_style", "options"); + node.getProperties().put(PROP_API_STYLE, "options"); nodes.add(node); componentNames.add(name); } @@ -92,9 +96,9 @@ public DetectorResult detect(DetectorContext ctx) { while (m.find()) { String compName = extractScriptSetupName(filePath); if (compName == null || componentNames.contains(compName)) continue; - CodeNode node = FrontendDetectorHelper.createComponentNode("vue", filePath, "component", + CodeNode node = FrontendDetectorHelper.createComponentNode(PROP_VUE, filePath, PROP_COMPONENT, compName, NodeKind.COMPONENT, FrontendDetectorHelper.lineAt(text, m.start())); - node.getProperties().put("api_style", "script_setup"); + node.getProperties().put(PROP_API_STYLE, "script_setup"); nodes.add(node); componentNames.add(compName); } @@ -106,7 +110,7 @@ public DetectorResult detect(DetectorContext ctx) { while (hm.find()) { String name = hm.group(1); if (hookNames.contains(name)) continue; - nodes.add(FrontendDetectorHelper.createComponentNode("vue", filePath, "hook", + nodes.add(FrontendDetectorHelper.createComponentNode(PROP_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/go/GoOrmDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/go/GoOrmDetector.java index f0e0d4a2..ad675b26 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/go/GoOrmDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/go/GoOrmDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.go; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -30,6 +29,13 @@ ) @Component public class GoOrmDetector extends AbstractAntlrDetector { + private static final String PROP_DATABASE_SQL = "database_sql"; + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_GORM = "gorm"; + private static final String PROP_OP = "op"; + private static final String PROP_OPERATION = "operation"; + private static final String PROP_SQLX = "sqlx"; + private static final Pattern GORM_MODEL_RE = Pattern.compile("type\\s+(?\\w+)\\s+struct\\s*\\{[^}]*gorm\\.Model", Pattern.DOTALL); private static final Pattern GORM_MIGRATE_RE = Pattern.compile("\\.AutoMigrate\\s*\\(", Pattern.MULTILINE); @@ -53,9 +59,9 @@ public Set getSupportedLanguages() { } private static String detectOrm(String text) { - if (HAS_GORM_RE.matcher(text).find()) return "gorm"; - if (HAS_SQLX_RE.matcher(text).find()) return "sqlx"; - if (HAS_DATABASE_SQL_RE.matcher(text).find()) return "database_sql"; + if (HAS_GORM_RE.matcher(text).find()) return PROP_GORM; + if (HAS_SQLX_RE.matcher(text).find()) return PROP_SQLX; + if (HAS_DATABASE_SQL_RE.matcher(text).find()) return PROP_DATABASE_SQL; return null; } @Override @@ -87,7 +93,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(filePath + "::" + modelName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "gorm"); + node.getProperties().put(PROP_FRAMEWORK, PROP_GORM); node.getProperties().put("type", "model"); nodes.add(node); } @@ -103,7 +109,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(filePath + "::AutoMigrate"); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "gorm"); + node.getProperties().put(PROP_FRAMEWORK, PROP_GORM); node.getProperties().put("type", "auto_migrate"); nodes.add(node); } @@ -112,7 +118,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { if ("gorm".equals(orm)) { m = GORM_QUERY_RE.matcher(text); while (m.find()) { - String op = m.group("op"); + String op = m.group(PROP_OP); int line = findLineNumber(text, m.start()); String sourceId = "go_orm:" + filePath + ":query:" + op + ":" + line; CodeEdge edge = new CodeEdge(); @@ -120,8 +126,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { edge.setKind(EdgeKind.QUERIES); edge.setSourceId(filePath); edge.setTarget(new CodeNode(sourceId, NodeKind.QUERY, "gorm." + op)); - edge.getProperties().put("framework", "gorm"); - edge.getProperties().put("operation", op); + edge.getProperties().put(PROP_FRAMEWORK, PROP_GORM); + edge.getProperties().put(PROP_OPERATION, op); edges.add(edge); } } @@ -129,7 +135,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { // sqlx connections m = SQLX_CONNECT_RE.matcher(text); while (m.find()) { - String op = m.group("op"); + String op = m.group(PROP_OP); int line = findLineNumber(text, m.start()); CodeNode node = new CodeNode(); node.setId("go_orm:" + filePath + ":connection:sqlx:" + line); @@ -138,8 +144,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(filePath + "::sqlx." + op); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "sqlx"); - node.getProperties().put("operation", op); + node.getProperties().put(PROP_FRAMEWORK, PROP_SQLX); + node.getProperties().put(PROP_OPERATION, op); nodes.add(node); } @@ -147,7 +153,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { if ("sqlx".equals(orm)) { m = SQLX_QUERY_RE.matcher(text); while (m.find()) { - String op = m.group("op"); + String op = m.group(PROP_OP); int line = findLineNumber(text, m.start()); String targetId = "go_orm:" + filePath + ":query:sqlx:" + op + ":" + line; CodeEdge edge = new CodeEdge(); @@ -155,8 +161,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { edge.setKind(EdgeKind.QUERIES); edge.setSourceId(filePath); edge.setTarget(new CodeNode(targetId, NodeKind.QUERY, "sqlx." + op)); - edge.getProperties().put("framework", "sqlx"); - edge.getProperties().put("operation", op); + edge.getProperties().put(PROP_FRAMEWORK, PROP_SQLX); + edge.getProperties().put(PROP_OPERATION, op); edges.add(edge); } } @@ -172,8 +178,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(filePath + "::sql.Open"); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "database_sql"); - node.getProperties().put("operation", "Open"); + node.getProperties().put(PROP_FRAMEWORK, PROP_DATABASE_SQL); + node.getProperties().put(PROP_OPERATION, "Open"); nodes.add(node); } @@ -181,7 +187,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { if ("database_sql".equals(orm)) { m = SQL_QUERY_RE.matcher(text); while (m.find()) { - String op = m.group("op"); + String op = m.group(PROP_OP); int line = findLineNumber(text, m.start()); String targetId = "go_orm:" + filePath + ":query:sql:" + op + ":" + line; CodeEdge edge = new CodeEdge(); @@ -189,8 +195,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { edge.setKind(EdgeKind.QUERIES); edge.setSourceId(filePath); edge.setTarget(new CodeNode(targetId, NodeKind.QUERY, "sql." + op)); - edge.getProperties().put("framework", "database_sql"); - edge.getProperties().put("operation", op); + edge.getProperties().put(PROP_FRAMEWORK, PROP_DATABASE_SQL); + edge.getProperties().put(PROP_OPERATION, op); edges.add(edge); } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetector.java index 647b8380..b940cef3 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.go; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -27,6 +26,8 @@ ) @Component public class GoStructuresDetector extends AbstractAntlrDetector { + private static final String PROP_EXPORTED = "exported"; + private static final Pattern STRUCT_RE = Pattern.compile("type\\s+(\\w+)\\s+struct\\s*\\{"); private static final Pattern INTERFACE_RE = Pattern.compile("type\\s+(\\w+)\\s+interface\\s*\\{"); @@ -115,7 +116,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(pkgName != null ? pkgName + "." + name : name); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, sm.start())); - node.getProperties().put("exported", exported); + node.getProperties().put(PROP_EXPORTED, exported); node.getProperties().put("type", "struct"); nodes.add(node); } @@ -132,7 +133,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(pkgName != null ? pkgName + "." + name : name); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, ifm.start())); - node.getProperties().put("exported", exported); + node.getProperties().put(PROP_EXPORTED, exported); nodes.add(node); } @@ -151,7 +152,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(pkgName != null ? pkgName + "." + receiver + "." + methodName : receiver + "." + methodName); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, mm.start())); - node.getProperties().put("exported", exported); + node.getProperties().put(PROP_EXPORTED, exported); node.getProperties().put("receiver_type", receiver); nodes.add(node); @@ -181,7 +182,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(pkgName != null ? pkgName + "." + funcName : funcName); node.setFilePath(filePath); node.setLineStart(findLineNumber(text, fm.start())); - node.getProperties().put("exported", exported); + node.getProperties().put(PROP_EXPORTED, exported); node.getProperties().put("type", "function"); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/go/GoWebDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/go/GoWebDetector.java index f082c149..2be22027 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/go/GoWebDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/go/GoWebDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.go; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeNode; @@ -25,6 +24,10 @@ ) @Component public class GoWebDetector extends AbstractAntlrDetector { + private static final String PROP_METHOD = "method"; + private static final String PROP_MUX = "mux"; + private static final String PROP_PATH = "path"; + private static final Pattern UPPER_ROUTE_RE = Pattern.compile("\\.(?GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\\s*\\(\\s*\"(?[^\"]*)\"", Pattern.MULTILINE); private static final Pattern LOWER_ROUTE_RE = Pattern.compile("\\.(?Get|Post|Put|Delete|Patch|Head|Options)\\s*\\(\\s*\"(?[^\"]*)\"", Pattern.MULTILINE); @@ -51,7 +54,7 @@ private static String detectFramework(String text) { if (GIN_RE.matcher(text).find()) return "gin"; if (ECHO_RE.matcher(text).find()) return "echo"; if (CHI_RE.matcher(text).find()) return "chi"; - if (MUX_RE.matcher(text).find()) return "mux"; + if (MUX_RE.matcher(text).find()) return PROP_MUX; return "net_http"; } @Override @@ -73,8 +76,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { // Gin/Echo uppercase routes Matcher m = UPPER_ROUTE_RE.matcher(text); while (m.find()) { - String method = m.group("method"); - String path = m.group("path"); + String method = m.group(PROP_METHOD); + String path = m.group(PROP_PATH); int line = findLineNumber(text, m.start()); nodes.add(endpointNode(filePath, method, path, line, framework)); } @@ -82,8 +85,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { // Chi lowercase routes m = LOWER_ROUTE_RE.matcher(text); while (m.find()) { - String method = m.group("method").toUpperCase(); - String path = m.group("path"); + String method = m.group(PROP_METHOD).toUpperCase(); + String path = m.group(PROP_PATH); int line = findLineNumber(text, m.start()); nodes.add(endpointNode(filePath, method, path, line, "chi")); } @@ -92,10 +95,10 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { Set handleFuncWithMethodPositions = new HashSet<>(); m = HANDLEFUNC_RE.matcher(text); while (m.find()) { - String method = m.group("method"); - String path = m.group("path"); + String method = m.group(PROP_METHOD); + String path = m.group(PROP_PATH); int line = findLineNumber(text, m.start()); - nodes.add(endpointNode(filePath, method, path, line, "mux")); + nodes.add(endpointNode(filePath, method, path, line, PROP_MUX)); handleFuncWithMethodPositions.add(m.start()); } @@ -104,16 +107,16 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { m = HANDLEFUNC_NO_METHOD_RE.matcher(text); while (m.find()) { if (handleFuncWithMethodPositions.contains(m.start())) continue; - String path = m.group("path"); + String path = m.group(PROP_PATH); int line = findLineNumber(text, m.start()); - nodes.add(endpointNode(filePath, "ANY", path, line, "mux")); + nodes.add(endpointNode(filePath, "ANY", path, line, PROP_MUX)); } } // net/http Handle/HandleFunc m = HTTP_HANDLE_RE.matcher(text); while (m.find()) { - String path = m.group("path"); + String path = m.group(PROP_PATH); int line = findLineNumber(text, m.start()); nodes.add(endpointNode(filePath, "ANY", path, line, "net_http")); } @@ -148,7 +151,7 @@ private static CodeNode endpointNode(String filePath, String method, String path node.setLineStart(line); node.getProperties().put("framework", fw); node.getProperties().put("http_method", method); - node.getProperties().put("path", path); + node.getProperties().put(PROP_PATH, path); return node; } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/iac/TerraformDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/iac/TerraformDetector.java index 09cf468a..2f0f8da7 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/iac/TerraformDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/iac/TerraformDetector.java @@ -27,6 +27,9 @@ ) @Component public class TerraformDetector extends AbstractRegexDetector { + private static final String PROP_PROVIDER = "provider"; + private static final String PROP_RESOURCE_TYPE = "resource_type"; + private static final Pattern RESOURCE_RE = Pattern.compile("resource\\s+\"([^\"]+)\"\\s+\"([^\"]+)\""); private static final Pattern DATA_RE = Pattern.compile("data\\s+\"([^\"]+)\"\\s+\"([^\"]+)\""); @@ -58,8 +61,8 @@ public DetectorResult detect(DetectorContext ctx) { n.setKind(NodeKind.INFRA_RESOURCE); n.setLabel(resourceType + "." + resourceName); n.setFqn(resourceType + "." + resourceName); n.setFilePath(ctx.filePath()); n.setLineStart(findLineNumber(text, m.start())); - n.getProperties().put("resource_type", resourceType); - if (provider != null) n.getProperties().put("provider", provider); + n.getProperties().put(PROP_RESOURCE_TYPE, resourceType); + if (provider != null) n.getProperties().put(PROP_PROVIDER, provider); nodes.add(n); } @@ -71,8 +74,8 @@ public DetectorResult detect(DetectorContext ctx) { n.setKind(NodeKind.INFRA_RESOURCE); n.setLabel("data." + dataType + "." + dataName); n.setFqn("data." + dataType + "." + dataName); n.setFilePath(ctx.filePath()); n.setLineStart(findLineNumber(text, m.start())); - n.getProperties().put("resource_type", dataType); n.getProperties().put("data_source", true); - if (provider != null) n.getProperties().put("provider", provider); + n.getProperties().put(PROP_RESOURCE_TYPE, dataType); n.getProperties().put("data_source", true); + if (provider != null) n.getProperties().put(PROP_PROVIDER, provider); nodes.add(n); } @@ -119,7 +122,7 @@ public DetectorResult detect(DetectorContext ctx) { n.setKind(NodeKind.INFRA_RESOURCE); n.setLabel("provider." + m.group(1)); n.setFqn("provider." + m.group(1)); n.setFilePath(ctx.filePath()); n.setLineStart(findLineNumber(text, m.start())); - n.getProperties().put("resource_type", "provider"); n.getProperties().put("provider", m.group(1)); + n.getProperties().put(PROP_RESOURCE_TYPE, PROP_PROVIDER); n.getProperties().put(PROP_PROVIDER, m.group(1)); nodes.add(n); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/AzureFunctionsDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/AzureFunctionsDetector.java index 198a6cff..55fa00e1 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/AzureFunctionsDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/AzureFunctionsDetector.java @@ -28,6 +28,9 @@ ) @Component public class AzureFunctionsDetector extends AbstractRegexDetector { + private static final String PROP_BROKER = "broker"; + private static final String PROP_TRIGGER_TYPE = "trigger_type"; + private static final Pattern FUNCTION_NAME_RE = Pattern.compile("@FunctionName\\s*\\(\\s*\"([^\"]+)\""); private static final Pattern HTTP_TRIGGER_RE = Pattern.compile("@HttpTrigger\\s*\\("); @@ -79,7 +82,7 @@ public DetectorResult detect(DetectorContext ctx) { Map props = new LinkedHashMap<>(); if (HTTP_TRIGGER_RE.matcher(contextLines).find()) { - props.put("trigger_type", "http"); + props.put(PROP_TRIGGER_TYPE, "http"); nodes.add(funcNode(funcNodeId, funcName, className, i + 1, ctx, List.of("@FunctionName", "@HttpTrigger"), props)); @@ -102,14 +105,14 @@ public DetectorResult detect(DetectorContext ctx) { Matcher sbq = SB_QUEUE_RE.matcher(contextLines); if (sbq.find()) { String queueName = sbq.group(1); - props.put("trigger_type", "serviceBusQueue"); + props.put(PROP_TRIGGER_TYPE, "serviceBusQueue"); props.put("queue_name", queueName); nodes.add(funcNode(funcNodeId, funcName, className, i + 1, ctx, List.of("@FunctionName", "@ServiceBusQueueTrigger"), props)); String queueNodeId = "azure:servicebus:queue:" + queueName; CodeNode qNode = new CodeNode(queueNodeId, NodeKind.QUEUE, "servicebus:" + queueName); - qNode.getProperties().put("broker", "azure_servicebus"); + qNode.getProperties().put(PROP_BROKER, "azure_servicebus"); qNode.getProperties().put("queue", queueName); nodes.add(qNode); addEdge(queueNodeId, funcNodeId, EdgeKind.TRIGGERS, @@ -120,14 +123,14 @@ public DetectorResult detect(DetectorContext ctx) { Matcher sbt = SB_TOPIC_RE.matcher(contextLines); if (sbt.find()) { String topicName = sbt.group(1); - props.put("trigger_type", "serviceBusTopic"); + props.put(PROP_TRIGGER_TYPE, "serviceBusTopic"); props.put("topic_name", topicName); nodes.add(funcNode(funcNodeId, funcName, className, i + 1, ctx, List.of("@FunctionName", "@ServiceBusTopicTrigger"), props)); String topicNodeId = "azure:servicebus:topic:" + topicName; CodeNode tNode = new CodeNode(topicNodeId, NodeKind.TOPIC, "servicebus:" + topicName); - tNode.getProperties().put("broker", "azure_servicebus"); + tNode.getProperties().put(PROP_BROKER, "azure_servicebus"); tNode.getProperties().put("topic", topicName); nodes.add(tNode); addEdge(topicNodeId, funcNodeId, EdgeKind.TRIGGERS, @@ -138,14 +141,14 @@ public DetectorResult detect(DetectorContext ctx) { Matcher ehm = EH_TRIGGER_RE.matcher(contextLines); if (ehm.find()) { String hubName = ehm.group(1); - props.put("trigger_type", "eventHub"); + props.put(PROP_TRIGGER_TYPE, "eventHub"); props.put("event_hub_name", hubName); nodes.add(funcNode(funcNodeId, funcName, className, i + 1, ctx, List.of("@FunctionName", "@EventHubTrigger"), props)); String hubNodeId = "azure:eventhub:" + hubName; CodeNode hNode = new CodeNode(hubNodeId, NodeKind.TOPIC, "eventhub:" + hubName); - hNode.getProperties().put("broker", "azure_eventhub"); + hNode.getProperties().put(PROP_BROKER, "azure_eventhub"); hNode.getProperties().put("event_hub", hubName); nodes.add(hNode); addEdge(hubNodeId, funcNodeId, EdgeKind.TRIGGERS, @@ -156,7 +159,7 @@ public DetectorResult detect(DetectorContext ctx) { Matcher tm = TIMER_RE.matcher(contextLines); if (tm.find()) { String schedule = tm.group(1); - props.put("trigger_type", "timer"); + props.put(PROP_TRIGGER_TYPE, "timer"); props.put("schedule", schedule); nodes.add(funcNode(funcNodeId, funcName, className, i + 1, ctx, List.of("@FunctionName", "@TimerTrigger"), props)); @@ -164,7 +167,7 @@ public DetectorResult detect(DetectorContext ctx) { } if (COSMOS_TRIGGER_RE.matcher(contextLines).find()) { - props.put("trigger_type", "cosmosDB"); + props.put(PROP_TRIGGER_TYPE, "cosmosDB"); nodes.add(funcNode(funcNodeId, funcName, className, i + 1, ctx, List.of("@FunctionName", "@CosmosDBTrigger"), props)); @@ -178,7 +181,7 @@ public DetectorResult detect(DetectorContext ctx) { continue; } - props.put("trigger_type", "unknown"); + props.put(PROP_TRIGGER_TYPE, "unknown"); nodes.add(funcNode(funcNodeId, funcName, className, i + 1, ctx, List.of("@FunctionName"), props)); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/AzureMessagingDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/AzureMessagingDetector.java index 2216ba3d..386d4b78 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/AzureMessagingDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/AzureMessagingDetector.java @@ -28,6 +28,14 @@ ) @Component public class AzureMessagingDetector extends AbstractRegexDetector { + private static final String PROP_AZURE_EVENTHUB = "azure_eventhub"; + private static final String PROP_AZURE_SERVICEBUS = "azure_servicebus"; + private static final String PROP_BROKER = "broker"; + private static final String PROP_EVENT_HUB = "event_hub"; + private static final String PROP_QUEUE = "queue"; + private static final String PROP_ROLE = "role"; + private static final String PROP_TOPIC = "topic"; + private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); private static final Pattern SB_SENDER_CLIENT_RE = Pattern.compile("\\bServiceBusSenderClient\\b"); @@ -102,53 +110,53 @@ public DetectorResult detect(DetectorContext ctx) { for (String qname : queueNames) { String queueId = ensureSbQueueNode(qname, seenSbQueues, nodes); if (isSbSender) addMessagingEdge(classNodeId, queueId, EdgeKind.SENDS_TO, - className + " sends to " + qname, Map.of("queue", qname), edges); + className + " sends to " + qname, Map.of(PROP_QUEUE, qname), edges); if (isSbReceiver) addMessagingEdge(classNodeId, queueId, EdgeKind.RECEIVES_FROM, - className + " receives from " + qname, Map.of("queue", qname), edges); + className + " receives from " + qname, Map.of(PROP_QUEUE, qname), edges); } for (String tname : topicNames) { String topicId = ensureSbTopicNode(tname, seenSbTopics, nodes); if (isSbSender) addMessagingEdge(classNodeId, topicId, EdgeKind.SENDS_TO, - className + " sends to " + tname, Map.of("topic", tname), edges); + className + " sends to " + tname, Map.of(PROP_TOPIC, tname), edges); if (isSbReceiver) addMessagingEdge(classNodeId, topicId, EdgeKind.RECEIVES_FROM, - className + " receives from " + tname, Map.of("topic", tname), edges); + className + " receives from " + tname, Map.of(PROP_TOPIC, tname), edges); } for (String ehname : ehNames) { String ehId = ensureEventhubNode(ehname, seenEventHubs, nodes); if (isEhProducer) addMessagingEdge(classNodeId, ehId, EdgeKind.SENDS_TO, - className + " sends to " + ehname, Map.of("event_hub", ehname), edges); + className + " sends to " + ehname, Map.of(PROP_EVENT_HUB, ehname), edges); if (isEhConsumer) addMessagingEdge(classNodeId, ehId, EdgeKind.RECEIVES_FROM, - className + " receives from " + ehname, Map.of("event_hub", ehname), edges); + className + " receives from " + ehname, Map.of(PROP_EVENT_HUB, ehname), edges); } // Generic fallbacks if (isSbSender && queueNames.isEmpty() && topicNames.isEmpty()) { nodes.add(genericNode("azure:servicebus:__sender__", NodeKind.QUEUE, "azure:servicebus:sender", - Map.of("broker", "azure_servicebus", "role", "sender"))); + Map.of(PROP_BROKER, PROP_AZURE_SERVICEBUS, PROP_ROLE, "sender"))); addMessagingEdge(classNodeId, "azure:servicebus:__sender__", EdgeKind.SENDS_TO, className + " sends to Azure Service Bus", Map.of(), edges); } else if (isSbReceiver && queueNames.isEmpty() && topicNames.isEmpty()) { nodes.add(genericNode("azure:servicebus:__receiver__", NodeKind.QUEUE, "azure:servicebus:receiver", - Map.of("broker", "azure_servicebus", "role", "receiver"))); + Map.of(PROP_BROKER, PROP_AZURE_SERVICEBUS, PROP_ROLE, "receiver"))); addMessagingEdge(classNodeId, "azure:servicebus:__receiver__", EdgeKind.RECEIVES_FROM, className + " receives from Azure Service Bus", Map.of(), edges); } else if (hasSbClient && queueNames.isEmpty() && topicNames.isEmpty() && !isSbSender && !isSbReceiver) { nodes.add(genericNode("azure:servicebus:__client__", NodeKind.QUEUE, "azure:servicebus:client", - Map.of("broker", "azure_servicebus", "role", "client"))); + Map.of(PROP_BROKER, PROP_AZURE_SERVICEBUS, PROP_ROLE, "client"))); addMessagingEdge(classNodeId, "azure:servicebus:__client__", EdgeKind.CONNECTS_TO, className + " connects to Azure Service Bus", Map.of(), edges); } if (isEhProducer && ehNames.isEmpty()) { nodes.add(genericNode("azure:eventhub:__producer__", NodeKind.TOPIC, "azure:eventhub:producer", - Map.of("broker", "azure_eventhub", "role", "producer"))); + Map.of(PROP_BROKER, PROP_AZURE_EVENTHUB, PROP_ROLE, "producer"))); addMessagingEdge(classNodeId, "azure:eventhub:__producer__", EdgeKind.SENDS_TO, className + " sends to Azure Event Hub", Map.of(), edges); } else if (isEhConsumer && ehNames.isEmpty()) { nodes.add(genericNode("azure:eventhub:__consumer__", NodeKind.TOPIC, "azure:eventhub:consumer", - Map.of("broker", "azure_eventhub", "role", "consumer"))); + Map.of(PROP_BROKER, PROP_AZURE_EVENTHUB, PROP_ROLE, "consumer"))); addMessagingEdge(classNodeId, "azure:eventhub:__consumer__", EdgeKind.RECEIVES_FROM, className + " receives from Azure Event Hub", Map.of(), edges); } @@ -161,7 +169,7 @@ private String ensureSbQueueNode(String name, Set seen, List n if (!seen.contains(name)) { seen.add(name); nodes.add(genericNode(nodeId, NodeKind.QUEUE, "azure:servicebus:" + name, - Map.of("broker", "azure_servicebus", "queue", name))); + Map.of(PROP_BROKER, PROP_AZURE_SERVICEBUS, PROP_QUEUE, name))); } return nodeId; } @@ -171,7 +179,7 @@ private String ensureSbTopicNode(String name, Set seen, List n if (!seen.contains(name)) { seen.add(name); nodes.add(genericNode(nodeId, NodeKind.TOPIC, "azure:servicebus:" + name, - Map.of("broker", "azure_servicebus", "topic", name))); + Map.of(PROP_BROKER, PROP_AZURE_SERVICEBUS, PROP_TOPIC, name))); } return nodeId; } @@ -181,7 +189,7 @@ private String ensureEventhubNode(String name, Set seen, List if (!seen.contains(name)) { seen.add(name); nodes.add(genericNode(nodeId, NodeKind.TOPIC, "azure:eventhub:" + name, - Map.of("broker", "azure_eventhub", "event_hub", name))); + Map.of(PROP_BROKER, PROP_AZURE_EVENTHUB, PROP_EVENT_HUB, name))); } return nodeId; } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/ClassHierarchyDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/ClassHierarchyDetector.java index 8ba6fed1..279f6e9f 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/ClassHierarchyDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/ClassHierarchyDetector.java @@ -34,6 +34,13 @@ ) @Component public class ClassHierarchyDetector extends AbstractJavaParserDetector { + private static final String PROP_PROTECTED = "protected"; + + + private static final String PROP_INTERFACES = "interfaces"; + private static final String PROP_IS_ABSTRACT = "is_abstract"; + private static final String PROP_IS_FINAL = "is_final"; + private static final String PROP_VISIBILITY = "visibility"; // ---- Regex patterns for fallback ---- private static final Pattern CLASS_DECL_RE = Pattern.compile( @@ -100,9 +107,9 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { } Map props = new LinkedHashMap<>(); - props.put("visibility", visibility); - props.put("is_abstract", isAbstract); - props.put("is_final", isFinal); + props.put(PROP_VISIBILITY, visibility); + props.put(PROP_IS_ABSTRACT, isAbstract); + props.put(PROP_IS_FINAL, isFinal); // Extended types List extendedTypes = new ArrayList<>(); @@ -111,7 +118,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { } if (!extendedTypes.isEmpty()) { if (isInterface) { - props.put("interfaces", extendedTypes); + props.put(PROP_INTERFACES, extendedTypes); } else { props.put("superclass", extendedTypes.get(0)); } @@ -123,7 +130,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { implementedTypes.add(impl.getNameAsString()); } if (!implementedTypes.isEmpty()) { - props.put("interfaces", implementedTypes); + props.put(PROP_INTERFACES, implementedTypes); } CodeNode node = new CodeNode(); @@ -179,7 +186,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { int lineEnd = decl.getEnd().map(p -> p.line).orElse(line); String visibility = decl.isPublic() ? "public" - : decl.isProtected() ? "protected" + : decl.isProtected() ? PROP_PROTECTED : decl.isPrivate() ? "private" : "package-private"; @@ -189,10 +196,10 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { } Map props = new LinkedHashMap<>(); - props.put("visibility", visibility); - props.put("is_abstract", false); - props.put("is_final", false); - if (!interfaces.isEmpty()) props.put("interfaces", interfaces); + props.put(PROP_VISIBILITY, visibility); + props.put(PROP_IS_ABSTRACT, false); + props.put(PROP_IS_FINAL, false); + if (!interfaces.isEmpty()) props.put(PROP_INTERFACES, interfaces); CodeNode node = new CodeNode(); node.setId(nodeId); @@ -224,14 +231,14 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { int lineEnd = decl.getEnd().map(p -> p.line).orElse(line); String visibility = decl.isPublic() ? "public" - : decl.isProtected() ? "protected" + : decl.isProtected() ? PROP_PROTECTED : decl.isPrivate() ? "private" : "package-private"; Map props = new LinkedHashMap<>(); - props.put("visibility", visibility); - props.put("is_abstract", false); - props.put("is_final", false); + props.put(PROP_VISIBILITY, visibility); + props.put(PROP_IS_ABSTRACT, false); + props.put(PROP_IS_FINAL, false); CodeNode node = new CodeNode(); node.setId(nodeId); @@ -250,7 +257,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { private String resolveVisibility(ClassOrInterfaceDeclaration decl) { if (decl.isPublic()) return "public"; - if (decl.isProtected()) return "protected"; + if (decl.isProtected()) return PROP_PROTECTED; if (decl.isPrivate()) return "private"; return "package-private"; } @@ -278,11 +285,11 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { NodeKind kind = isAbstract ? NodeKind.ABSTRACT_CLASS : NodeKind.CLASS; Map props = new LinkedHashMap<>(); - props.put("visibility", visibility); - props.put("is_abstract", isAbstract); - props.put("is_final", isFinal); + props.put(PROP_VISIBILITY, visibility); + props.put(PROP_IS_ABSTRACT, isAbstract); + props.put(PROP_IS_FINAL, isFinal); if (superclass != null) props.put("superclass", superclass); - if (!interfaces.isEmpty()) props.put("interfaces", interfaces); + if (!interfaces.isEmpty()) props.put(PROP_INTERFACES, interfaces); CodeNode node = new CodeNode(); node.setId(nodeId); @@ -322,10 +329,10 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { String nodeId = ctx.filePath() + ":" + name; Map props = new LinkedHashMap<>(); - props.put("visibility", visibility); - props.put("is_abstract", false); - props.put("is_final", false); - if (!extended.isEmpty()) props.put("interfaces", extended); + props.put(PROP_VISIBILITY, visibility); + props.put(PROP_IS_ABSTRACT, false); + props.put(PROP_IS_FINAL, false); + if (!extended.isEmpty()) props.put(PROP_INTERFACES, extended); CodeNode node = new CodeNode(); node.setId(nodeId); @@ -357,10 +364,10 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { String nodeId = ctx.filePath() + ":" + name; Map props = new LinkedHashMap<>(); - props.put("visibility", visibility); - props.put("is_abstract", false); - props.put("is_final", false); - if (!interfaces.isEmpty()) props.put("interfaces", interfaces); + props.put(PROP_VISIBILITY, visibility); + props.put(PROP_IS_ABSTRACT, false); + props.put(PROP_IS_FINAL, false); + if (!interfaces.isEmpty()) props.put(PROP_INTERFACES, interfaces); CodeNode node = new CodeNode(); node.setId(nodeId); @@ -391,9 +398,9 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { String nodeId = ctx.filePath() + ":" + name; Map props = new LinkedHashMap<>(); - props.put("visibility", visibility); - props.put("is_abstract", false); - props.put("is_final", false); + props.put(PROP_VISIBILITY, visibility); + props.put(PROP_IS_ABSTRACT, false); + props.put(PROP_IS_FINAL, false); CodeNode node = new CodeNode(); node.setId(nodeId); @@ -413,7 +420,7 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { private String parseVisibility(String modifier) { if (modifier == null) return "package-private"; String trimmed = modifier.trim(); - if (trimmed.equals("public") || trimmed.equals("protected") || trimmed.equals("private")) { + if (trimmed.equals("public") || trimmed.equals(PROP_PROTECTED) || trimmed.equals("private")) { return trimmed; } return "package-private"; diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/ConfigDefDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/ConfigDefDetector.java index 3d776c20..6ba7d641 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/ConfigDefDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/ConfigDefDetector.java @@ -4,10 +4,8 @@ import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; import com.github.javaparser.ast.body.FieldDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.body.Parameter; import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.expr.MethodCallExpr; -import com.github.javaparser.ast.expr.StringLiteralExpr; import io.github.randomcodespace.iq.detector.DetectorContext; import io.github.randomcodespace.iq.detector.DetectorResult; import io.github.randomcodespace.iq.model.CodeEdge; @@ -37,6 +35,9 @@ ) @Component public class ConfigDefDetector extends AbstractJavaParserDetector { + private static final String PROP_CONFIGDEF = "ConfigDef"; + private static final String PROP_SPRING_VALUE = "spring_value"; + // ---- Regex fallback patterns ---- private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); @@ -62,7 +63,7 @@ public DetectorResult detect(DetectorContext ctx) { String text = ctx.content(); if (text == null) return DetectorResult.empty(); - boolean hasConfigDef = text.contains("ConfigDef"); + boolean hasConfigDef = text.contains(PROP_CONFIGDEF); boolean hasValue = text.contains("@Value"); boolean hasConfigProps = text.contains("@ConfigurationProperties"); @@ -89,7 +90,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { String classNodeId = ctx.filePath() + ":" + className; // 1. Kafka ConfigDef.define() calls - // Discriminator: receiver must mention "ConfigDef" or "CONFIG" to avoid matching + // Discriminator: receiver must mention PROP_CONFIGDEF or "CONFIG" to avoid matching // unrelated .define() calls (per CLAUDE.md: framework detectors must have guards). classDecl.findAll(MethodCallExpr.class).forEach(call -> { if (!"define".equals(call.getNameAsString())) return; @@ -98,7 +99,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { boolean receiverIsConfigDef = call.getScope() .map(scope -> { String s = scope.toString(); - return s.contains("ConfigDef") || s.toUpperCase().contains("CONFIG"); + return s.contains(PROP_CONFIGDEF) || s.toUpperCase().contains("CONFIG"); }) .orElse(false); if (!receiverIsConfigDef) return; @@ -118,7 +119,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { extractValueKey(ann).ifPresent(key -> { if (seenKeys.add(key)) { int line = ann.getBegin().map(p -> p.line).orElse(1); - addConfigNode(key, "spring_value", classNodeId, ctx.filePath(), line, nodes, edges); + addConfigNode(key, PROP_SPRING_VALUE, classNodeId, ctx.filePath(), line, nodes, edges); } }); }); @@ -131,7 +132,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { extractValueKey(ann).ifPresent(key -> { if (seenKeys.add(key)) { int line = ann.getBegin().map(p -> p.line).orElse(1); - addConfigNode(key, "spring_value", classNodeId, ctx.filePath(), line, nodes, edges); + addConfigNode(key, PROP_SPRING_VALUE, classNodeId, ctx.filePath(), line, nodes, edges); } }); }); @@ -225,7 +226,7 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { while (vm.find()) { String key = vm.group(1); if (seenKeys.add(key)) { - addConfigNode(key, "spring_value", classNodeId, ctx.filePath(), i + 1, nodes, edges); + addConfigNode(key, PROP_SPRING_VALUE, classNodeId, ctx.filePath(), i + 1, nodes, edges); } } 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 9f9f298b..786a40fa 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 @@ -27,6 +27,10 @@ ) @Component public class IbmMqDetector extends AbstractJavaMessagingDetector { + private static final String PROP_BROKER = "broker"; + private static final String PROP_IBM_MQ = "ibm_mq"; + private static final String PROP_QUEUE = "queue"; + 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*\"([^\"]+)\""); @@ -38,7 +42,7 @@ public class IbmMqDetector extends AbstractJavaMessagingDetector { @Override public String getName() { - return "ibm_mq"; + return PROP_IBM_MQ; } @Override @@ -77,7 +81,7 @@ public DetectorResult detect(DetectorContext ctx) { if (m.find()) { String qmName = m.group(1); String qmId = ensureNode("ibmmq:qm:" + qmName, qmName, NodeKind.MESSAGE_QUEUE, - "ibmmq:qm:" + qmName, Map.of("broker", "ibm_mq", "queue_manager", qmName), + "ibmmq:qm:" + qmName, Map.of(PROP_BROKER, PROP_IBM_MQ, "queue_manager", qmName), seenQms, nodes); addMessagingEdge(classNodeId, qmId, EdgeKind.CONNECTS_TO, className + " connects to queue manager " + qmName, @@ -91,14 +95,14 @@ public DetectorResult detect(DetectorContext ctx) { if (m.find()) { String queueName = m.group(1); String queueId = ensureNode("ibmmq:queue:" + queueName, queueName, NodeKind.QUEUE, - "ibmmq:queue:" + queueName, Map.of("broker", "ibm_mq", "queue", queueName), + "ibmmq:queue:" + queueName, Map.of(PROP_BROKER, PROP_IBM_MQ, PROP_QUEUE, queueName), seenQueues, nodes); if (hasPut) addMessagingEdge(classNodeId, queueId, EdgeKind.SENDS_TO, - className + " sends to " + queueName, Map.of("queue", queueName), edges); + className + " sends to " + queueName, Map.of(PROP_QUEUE, queueName), edges); if (hasGet) addMessagingEdge(classNodeId, queueId, EdgeKind.RECEIVES_FROM, - className + " receives from " + queueName, Map.of("queue", queueName), edges); + className + " receives from " + queueName, Map.of(PROP_QUEUE, queueName), edges); if (!hasPut && !hasGet) addMessagingEdge(classNodeId, queueId, EdgeKind.CONNECTS_TO, - className + " accesses " + queueName, Map.of("queue", queueName), edges); + className + " accesses " + queueName, Map.of(PROP_QUEUE, queueName), edges); } } @@ -106,11 +110,11 @@ public DetectorResult detect(DetectorContext ctx) { for (int i = 0; i < lines.length; i++) { Matcher m = JMS_CREATE_QUEUE_RE.matcher(lines[i]); if (m.find()) ensureNode("ibmmq:queue:" + m.group(1), m.group(1), NodeKind.QUEUE, - "ibmmq:queue:" + m.group(1), Map.of("broker", "ibm_mq", "queue", m.group(1)), + "ibmmq:queue:" + m.group(1), Map.of(PROP_BROKER, PROP_IBM_MQ, PROP_QUEUE, m.group(1)), seenQueues, nodes); m = JMS_CREATE_TOPIC_RE.matcher(lines[i]); if (m.find()) ensureNode("ibmmq:topic:" + m.group(1), m.group(1), NodeKind.TOPIC, - "ibmmq:topic:" + m.group(1), Map.of("broker", "ibm_mq", "topic", m.group(1)), + "ibmmq:topic:" + m.group(1), Map.of(PROP_BROKER, PROP_IBM_MQ, "topic", m.group(1)), seenTopics, nodes); } @@ -119,7 +123,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setId("ibmmq:topic:__unknown__"); node.setKind(NodeKind.TOPIC); node.setLabel("ibmmq:topic:unknown"); - node.getProperties().put("broker", "ibm_mq"); + node.getProperties().put(PROP_BROKER, PROP_IBM_MQ); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/JdbcDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/JdbcDetector.java index 56efab65..74a739b9 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/JdbcDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/JdbcDetector.java @@ -28,6 +28,10 @@ ) @Component public class JdbcDetector extends AbstractRegexDetector { + private static final String PROP_DB_TYPE = "db_type"; + private static final String PROP_HOST = "host"; + private static final String PROP_UNKNOWN = "unknown"; + private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); private static final Pattern DRIVER_MANAGER_RE = Pattern.compile( @@ -94,8 +98,8 @@ public DetectorResult detect(DetectorContext ctx) { if (!m.find()) continue; String url = m.group(1); Map props = parseJdbcUrl(url); - String dbType = props.getOrDefault("db_type", "unknown"); - String host = props.getOrDefault("host", "unknown"); + String dbType = props.getOrDefault(PROP_DB_TYPE, PROP_UNKNOWN); + String host = props.getOrDefault(PROP_HOST, PROP_UNKNOWN); String dbId = "db:" + dbType + ":" + host; ensureDbNode(dbId, dbType + "@" + host, i + 1, ctx, new LinkedHashMap<>(props), seenDbs, nodes); addConnectEdge(classNodeId, dbId, className + " connects to " + dbType + "@" + host, edges, nodes); @@ -130,8 +134,8 @@ public DetectorResult detect(DetectorContext ctx) { if (!m.find()) continue; String url = m.group(1); Map props = parseJdbcUrl(url); - String dbType = props.getOrDefault("db_type", "unknown"); - String host = props.getOrDefault("host", "unknown"); + String dbType = props.getOrDefault(PROP_DB_TYPE, PROP_UNKNOWN); + String host = props.getOrDefault(PROP_HOST, PROP_UNKNOWN); String dbId = "db:" + dbType + ":" + host; ensureDbNode(dbId, dbType + "@" + host, i + 1, ctx, new LinkedHashMap<>(props), seenDbs, nodes); } @@ -146,8 +150,8 @@ public DetectorResult detect(DetectorContext ctx) { while (urlMatcher.find()) { String url = urlMatcher.group(1); Map props = parseJdbcUrl(url); - String dbType = props.getOrDefault("db_type", "unknown"); - String host = props.getOrDefault("host", "unknown"); + String dbType = props.getOrDefault(PROP_DB_TYPE, PROP_UNKNOWN); + String host = props.getOrDefault(PROP_HOST, PROP_UNKNOWN); String dbId = "db:" + dbType + ":" + host; ensureDbNode(dbId, dbType + "@" + host, i + 1, ctx, new LinkedHashMap<>(props), seenDbs, nodes); addConnectEdge(classNodeId, dbId, className + " connects to " + dbType + "@" + host, edges, nodes); @@ -162,9 +166,9 @@ private Map parseJdbcUrl(String url) { props.put("connection_url", url); Matcher m = JDBC_URL_RE.matcher(url); if (m.find()) { - props.put("db_type", m.group(1)); + props.put(PROP_DB_TYPE, m.group(1)); if (m.group(2) != null) { - props.put("host", m.group(2)); + props.put(PROP_HOST, m.group(2)); } } return props; 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 029c9577..a3d740f1 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 @@ -27,6 +27,9 @@ ) @Component public class JmsDetector extends AbstractJavaMessagingDetector { + private static final String PROP_DESTINATION = "destination"; + private static final String PROP_JMS = "jms"; + private static final Pattern JMS_LISTENER_RE = Pattern.compile( "@JmsListener\\s*\\(\\s*(?:.*?destination\\s*=\\s*)?\"([^\"]+)\""); @@ -36,7 +39,7 @@ public class JmsDetector extends AbstractJavaMessagingDetector { @Override public String getName() { - return "jms"; + return PROP_JMS; } @Override @@ -68,9 +71,9 @@ 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("jms", destination, seenQueues, nodes); + String queueId = ensureQueueNode(PROP_JMS, destination, seenQueues, nodes); Map props = new LinkedHashMap<>(); - props.put("destination", destination); + props.put(PROP_DESTINATION, destination); Matcher cf = CONTAINER_FACTORY_RE.matcher(lines[i]); if (cf.find()) props.put("container_factory", cf.group(1)); addMessagingEdge(classNodeId, queueId, EdgeKind.CONSUMES, @@ -82,10 +85,10 @@ 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("jms", destination, seenQueues, nodes); + String queueId = ensureQueueNode(PROP_JMS, destination, seenQueues, nodes); addMessagingEdge(classNodeId, queueId, EdgeKind.PRODUCES, className + " produces to " + destination, - Map.of("destination", destination), edges); + Map.of(PROP_DESTINATION, destination), edges); } return DetectorResult.of(nodes, edges); @@ -99,7 +102,7 @@ private String ensureQueueNode(String broker, String destination, Set se node.setKind(NodeKind.QUEUE); node.setLabel(broker + ":" + destination); node.getProperties().put("broker", broker); - node.getProperties().put("destination", destination); + node.getProperties().put(PROP_DESTINATION, destination); nodes.add(node); } return queueId; diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/JpaEntityDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/JpaEntityDetector.java index 7734c802..c6b2c8fa 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/JpaEntityDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/JpaEntityDetector.java @@ -38,6 +38,10 @@ ) @Component public class JpaEntityDetector extends AbstractJavaParserDetector { + private static final String PROP_FIELD = "field"; + private static final String PROP_NAME = "name"; + private static final String PROP_TYPE = "type"; + private static final Set RELATIONSHIP_ANNOTATIONS = Set.of( "OneToMany", "ManyToOne", "OneToOne", "ManyToMany"); @@ -88,7 +92,6 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { List edges = new ArrayList<>(); cu.findAll(ClassOrInterfaceDeclaration.class).forEach(classDecl -> { - // Only process @Entity annotated classes boolean isEntity = classDecl.getAnnotations().stream() .anyMatch(a -> "Entity".equals(a.getNameAsString())); if (!isEntity) return; @@ -96,39 +99,8 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { String className = classDecl.getNameAsString(); String fqn = resolveFqn(cu, className); int classLine = classDecl.getBegin().map(p -> p.line).orElse(1); - - // Extract table name from @Table annotation - String tableName = className.toLowerCase(); - for (AnnotationExpr ann : classDecl.getAnnotations()) { - if ("Table".equals(ann.getNameAsString())) { - String name = extractAnnotationStringAttr(ann, "name"); - if (name == null) { - // Try bare value - name = extractAnnotationValue(ann); - } - if (name != null) tableName = name; - } - } - - // Extract columns from fields - List> columns = new ArrayList<>(); - for (FieldDeclaration field : classDecl.getFields()) { - for (VariableDeclarator var : field.getVariables()) { - String fieldName = var.getNameAsString(); - String fieldType = var.getTypeAsString(); - - // Check for @Column annotation - for (AnnotationExpr ann : field.getAnnotations()) { - if ("Column".equals(ann.getNameAsString())) { - String colName = extractAnnotationStringAttr(ann, "name"); - if (colName == null) colName = fieldName; - columns.add(Map.of("name", colName, "field", fieldName, "type", fieldType)); - } else if ("Id".equals(ann.getNameAsString())) { - columns.add(Map.of("name", fieldName, "field", fieldName, "type", fieldType)); - } - } - } - } + String tableName = extractTableName(classDecl, className); + List> columns = extractColumns(classDecl); String entityId = ctx.filePath() + ":" + className; Map properties = new LinkedHashMap<>(); @@ -148,59 +120,92 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { nodes.add(node); DetectorDbHelper.addDbEdge(entityId, ctx.registry(), nodes, edges); - // Extract relationship edges from fields - for (FieldDeclaration field : classDecl.getFields()) { - for (AnnotationExpr ann : field.getAnnotations()) { - String annName = ann.getNameAsString(); - if (!RELATIONSHIP_ANNOTATIONS.contains(annName)) continue; + extractRelationshipEdges(classDecl, entityId, edges); + }); - String relType = RELATIONSHIP_TYPES.get(annName); + return DetectorResult.of(nodes, edges); + } - // Resolve target entity - String targetEntity = extractAnnotationStringAttr(ann, "targetEntity"); - if (targetEntity != null && targetEntity.endsWith(".class")) { - targetEntity = targetEntity.replace(".class", ""); - } + private String extractTableName(ClassOrInterfaceDeclaration classDecl, String className) { + for (AnnotationExpr ann : classDecl.getAnnotations()) { + if (!"Table".equals(ann.getNameAsString())) continue; + String name = extractAnnotationStringAttr(ann, PROP_NAME); + if (name == null) name = extractAnnotationValue(ann); + if (name != null) return name; + } + return className.toLowerCase(); + } - if (targetEntity == null) { - // Try to resolve from field type / generic type argument - for (VariableDeclarator var : field.getVariables()) { - Type type = var.getType(); - if (type.isClassOrInterfaceType()) { - ClassOrInterfaceType cit = type.asClassOrInterfaceType(); - if (cit.getTypeArguments().isPresent()) { - // Generic type like List -> Order - var typeArgs = cit.getTypeArguments().get(); - if (!typeArgs.isEmpty()) { - targetEntity = typeArgs.get(0).asString(); - } - } else { - targetEntity = cit.getNameAsString(); - } - break; - } - } - } + private List> extractColumns(ClassOrInterfaceDeclaration classDecl) { + List> columns = new ArrayList<>(); + for (FieldDeclaration field : classDecl.getFields()) { + for (VariableDeclarator var : field.getVariables()) { + addColumnFromAnnotations(field, var.getNameAsString(), var.getTypeAsString(), columns); + } + } + return columns; + } - if (targetEntity != null) { - String mappedBy = extractAnnotationStringAttr(ann, "mappedBy"); - Map edgeProps = new LinkedHashMap<>(); - edgeProps.put("relationship_type", relType); - if (mappedBy != null) edgeProps.put("mapped_by", mappedBy); - - CodeEdge edge = new CodeEdge(); - edge.setId(entityId + "->maps_to->*:" + targetEntity); - edge.setKind(EdgeKind.MAPS_TO); - edge.setSourceId(entityId); - edge.setTarget(new CodeNode("*:" + targetEntity, NodeKind.ENTITY, targetEntity)); - edge.setProperties(edgeProps); - edges.add(edge); - } - } + private void addColumnFromAnnotations(FieldDeclaration field, String fieldName, + String fieldType, List> columns) { + for (AnnotationExpr ann : field.getAnnotations()) { + if ("Column".equals(ann.getNameAsString())) { + String colName = extractAnnotationStringAttr(ann, PROP_NAME); + if (colName == null) colName = fieldName; + columns.add(Map.of(PROP_NAME, colName, PROP_FIELD, fieldName, PROP_TYPE, fieldType)); + } else if ("Id".equals(ann.getNameAsString())) { + columns.add(Map.of(PROP_NAME, fieldName, PROP_FIELD, fieldName, PROP_TYPE, fieldType)); } - }); + } + } - return DetectorResult.of(nodes, edges); + private void extractRelationshipEdges(ClassOrInterfaceDeclaration classDecl, + String entityId, List edges) { + for (FieldDeclaration field : classDecl.getFields()) { + for (AnnotationExpr ann : field.getAnnotations()) { + String annName = ann.getNameAsString(); + if (!RELATIONSHIP_ANNOTATIONS.contains(annName)) continue; + + String relType = RELATIONSHIP_TYPES.get(annName); + String targetEntity = resolveTargetEntity(ann, field); + if (targetEntity == null) continue; + + String mappedBy = extractAnnotationStringAttr(ann, "mappedBy"); + Map edgeProps = new LinkedHashMap<>(); + edgeProps.put("relationship_type", relType); + if (mappedBy != null) edgeProps.put("mapped_by", mappedBy); + + CodeEdge edge = new CodeEdge(); + edge.setId(entityId + "->maps_to->*:" + targetEntity); + edge.setKind(EdgeKind.MAPS_TO); + edge.setSourceId(entityId); + edge.setTarget(new CodeNode("*:" + targetEntity, NodeKind.ENTITY, targetEntity)); + edge.setProperties(edgeProps); + edges.add(edge); + } + } + } + + private String resolveTargetEntity(AnnotationExpr ann, FieldDeclaration field) { + String targetEntity = extractAnnotationStringAttr(ann, "targetEntity"); + if (targetEntity != null && targetEntity.endsWith(".class")) { + targetEntity = targetEntity.replace(".class", ""); + } + if (targetEntity != null) return targetEntity; + + for (VariableDeclarator var : field.getVariables()) { + Type type = var.getType(); + if (!type.isClassOrInterfaceType()) continue; + ClassOrInterfaceType cit = type.asClassOrInterfaceType(); + if (cit.getTypeArguments().isPresent()) { + var typeArgs = cit.getTypeArguments().get(); + if (!typeArgs.isEmpty()) return typeArgs.get(0).asString(); + } else { + return cit.getNameAsString(); + } + break; + } + return null; } private String extractAnnotationStringAttr(AnnotationExpr ann, String attrName) { @@ -256,7 +261,7 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { Matcher fm = FIELD_RE.matcher(lines[k]); if (fm.find()) { String colName = colNameMatch.find() ? colNameMatch.group(1) : fm.group(2); - columns.add(Map.of("name", colName, "field", fm.group(2), "type", fm.group(1).trim())); + columns.add(Map.of(PROP_NAME, colName, PROP_FIELD, fm.group(2), PROP_TYPE, fm.group(1).trim())); break; } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/KafkaDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/KafkaDetector.java index 737dd416..69866a2c 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/KafkaDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/KafkaDetector.java @@ -28,6 +28,8 @@ ) @Component public class KafkaDetector extends AbstractRegexDetector { + private static final String PROP_TOPIC = "topic"; + private static final Pattern CLASS_RE = Pattern.compile( "(?:(?:public|internal|private|protected|data|abstract|open|sealed|enum|inline|value)\\s+)*" + @@ -87,7 +89,7 @@ public DetectorResult detect(DetectorContext ctx) { String topic = fallback.group(1); String topicId = ensureTopicNode(topic, seenTopics, nodes, registry); Map props = new LinkedHashMap<>(); - props.put("topic", topic); + props.put(PROP_TOPIC, topic); addEdge(classNodeId, topicId, EdgeKind.CONSUMES, className + " consumes " + topic, props, edges, nodes); } @@ -97,7 +99,7 @@ public DetectorResult detect(DetectorContext ctx) { String topic = m.group(1); String topicId = ensureTopicNode(topic, seenTopics, nodes, registry); Map props = new LinkedHashMap<>(); - props.put("topic", topic); + props.put(PROP_TOPIC, topic); Matcher gm = GROUP_ID_RE.matcher(lines[i]); if (gm.find()) props.put("group_id", gm.group(1)); addEdge(classNodeId, topicId, EdgeKind.CONSUMES, @@ -112,7 +114,7 @@ public DetectorResult detect(DetectorContext ctx) { String topicId = ensureTopicNode(topic, seenTopics, nodes, registry); addEdge(classNodeId, topicId, EdgeKind.PRODUCES, className + " produces to " + topic, - Map.of("topic", topic), edges, nodes); + Map.of(PROP_TOPIC, topic), edges, nodes); } return DetectorResult.of(nodes, edges); @@ -133,7 +135,7 @@ private String ensureTopicNode(String topic, Set seen, List no node.setKind(NodeKind.TOPIC); node.setLabel("kafka:" + topic); node.getProperties().put("broker", "kafka"); - node.getProperties().put("topic", topic); + node.getProperties().put(PROP_TOPIC, topic); nodes.add(node); } return topicId; diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/MicronautDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/MicronautDetector.java index 9ac5a959..ae78b423 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/MicronautDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/MicronautDetector.java @@ -28,6 +28,9 @@ ) @Component public class MicronautDetector extends AbstractRegexDetector { + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_MICRONAUT = "micronaut"; + private static final Pattern CONTROLLER_RE = Pattern.compile("@Controller\\s*\\(\\s*\"([^\"]*)\""); private static final Pattern HTTP_METHOD_RE = Pattern.compile("@(Get|Post|Put|Delete)(?!Mapping)\\s*(?:\\(\\s*\"([^\"]*)\")?\\s*\\)?"); @@ -42,7 +45,7 @@ public class MicronautDetector extends AbstractRegexDetector { @Override public String getName() { - return "micronaut"; + return PROP_MICRONAUT; } @Override @@ -101,7 +104,7 @@ public DetectorResult detect(DetectorContext ctx) { ctrlNode.setFilePath(ctx.filePath()); ctrlNode.setLineStart(1); ctrlNode.getAnnotations().add("@Controller"); - ctrlNode.getProperties().put("framework", "micronaut"); + ctrlNode.getProperties().put(PROP_FRAMEWORK, PROP_MICRONAUT); ctrlNode.getProperties().put("path", controllerPath); nodes.add(ctrlNode); } @@ -137,7 +140,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setFilePath(ctx.filePath()); node.setLineStart(lineno); node.getAnnotations().add("@" + hm.group(1)); - node.getProperties().put("framework", "micronaut"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MICRONAUT); node.getProperties().put("http_method", httpMethod); node.getProperties().put("path", fullPath); nodes.add(node); @@ -163,7 +166,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setFilePath(ctx.filePath()); node.setLineStart(lineno); node.getAnnotations().add("@" + scope); - node.getProperties().put("framework", "micronaut"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MICRONAUT); node.getProperties().put("bean_scope", scope); nodes.add(node); } @@ -181,7 +184,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setFilePath(ctx.filePath()); node.setLineStart(lineno); node.getAnnotations().add("@Client"); - node.getProperties().put("framework", "micronaut"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MICRONAUT); node.getProperties().put("client_target", clientTarget); nodes.add(node); @@ -204,7 +207,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setFilePath(ctx.filePath()); node.setLineStart(lineno); node.getAnnotations().add("@Inject"); - node.getProperties().put("framework", "micronaut"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MICRONAUT); nodes.add(node); } @@ -221,7 +224,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setFilePath(ctx.filePath()); node.setLineStart(lineno); node.getAnnotations().add("@Scheduled"); - node.getProperties().put("framework", "micronaut"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MICRONAUT); node.getProperties().put("fixed_rate", rate); nodes.add(node); } @@ -237,7 +240,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setFilePath(ctx.filePath()); node.setLineStart(lineno); node.getAnnotations().add("@EventListener"); - node.getProperties().put("framework", "micronaut"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MICRONAUT); nodes.add(node); } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/ModuleDepsDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/ModuleDepsDetector.java index 3f842a78..1d1a9673 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/ModuleDepsDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/ModuleDepsDetector.java @@ -28,6 +28,10 @@ ) @Component public class ModuleDepsDetector extends AbstractRegexDetector { + private static final String PROP_BUILD_TOOL = "build_tool"; + private static final String PROP_GRADLE = "gradle"; + private static final String PROP_UNKNOWN = "unknown"; + private static final Pattern GRADLE_DEPENDENCY_RE = Pattern.compile( "(?:implementation|api|compile|compileOnly|runtimeOnly|testImplementation)\\s+" @@ -49,7 +53,7 @@ public String getName() { @Override public Set getSupportedLanguages() { - return Set.of("java", "xml", "gradle"); + return Set.of("java", "xml", PROP_GRADLE); } @Override @@ -78,9 +82,9 @@ private DetectorResult detectMaven(DetectorContext ctx) { if (depsIdx > 0) topSection = text.substring(0, depsIdx); Matcher gm = GROUP_ID_RE.matcher(topSection); - String groupId = gm.find() ? gm.group(1) : "unknown"; + String groupId = gm.find() ? gm.group(1) : PROP_UNKNOWN; Matcher am = ARTIFACT_ID_RE.matcher(topSection); - String artifactId = am.find() ? am.group(1) : "unknown"; + String artifactId = am.find() ? am.group(1) : PROP_UNKNOWN; String moduleId = "module:" + groupId + ":" + artifactId; CodeNode moduleNode = new CodeNode(); @@ -92,7 +96,7 @@ private DetectorResult detectMaven(DetectorContext ctx) { moduleNode.setLineStart(1); moduleNode.getProperties().put("group_id", groupId); moduleNode.getProperties().put("artifact_id", artifactId); - moduleNode.getProperties().put("build_tool", "maven"); + moduleNode.getProperties().put(PROP_BUILD_TOOL, "maven"); nodes.add(moduleNode); // Sub-modules @@ -104,7 +108,7 @@ private DetectorResult detectMaven(DetectorContext ctx) { subNode.setKind(NodeKind.MODULE); subNode.setLabel(subModule); subNode.setFqn(groupId + ":" + subModule); - subNode.getProperties().put("build_tool", "maven"); + subNode.getProperties().put(PROP_BUILD_TOOL, "maven"); subNode.getProperties().put("parent", artifactId); nodes.add(subNode); @@ -122,7 +126,7 @@ private DetectorResult detectMaven(DetectorContext ctx) { Matcher dg = GROUP_ID_RE.matcher(block); Matcher da = ARTIFACT_ID_RE.matcher(block); if (da.find()) { - String depGroup = dg.find() ? dg.group(1) : "unknown"; + String depGroup = dg.find() ? dg.group(1) : PROP_UNKNOWN; String depArtifact = da.group(1); String depId = "module:" + depGroup + ":" + depArtifact; CodeEdge edge = new CodeEdge(); @@ -167,7 +171,7 @@ private DetectorResult detectGradle(DetectorContext ctx) { moduleNode.setFqn(moduleName); moduleNode.setFilePath(ctx.filePath()); moduleNode.setLineStart(1); - moduleNode.getProperties().put("build_tool", "gradle"); + moduleNode.getProperties().put(PROP_BUILD_TOOL, PROP_GRADLE); nodes.add(moduleNode); for (int i = 0; i < lines.length; i++) { @@ -217,7 +221,7 @@ private DetectorResult detectGradleSettings(DetectorContext ctx) { node.setLabel(modulePath); node.setFqn(modulePath); node.setFilePath(ctx.filePath()); - node.getProperties().put("build_tool", "gradle"); + node.getProperties().put(PROP_BUILD_TOOL, PROP_GRADLE); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/QuarkusDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/QuarkusDetector.java index 5da73741..9a14109e 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/QuarkusDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/QuarkusDetector.java @@ -25,6 +25,9 @@ ) @Component public class QuarkusDetector extends AbstractRegexDetector { + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_QUARKUS = "quarkus"; + private static final Pattern QUARKUS_TEST_RE = Pattern.compile("@QuarkusTest\\b"); private static final Pattern CONFIG_PROPERTY_RE = Pattern.compile("@ConfigProperty\\s*\\(\\s*name\\s*=\\s*\"([^\"]+)\""); @@ -36,7 +39,7 @@ public class QuarkusDetector extends AbstractRegexDetector { @Override public String getName() { - return "quarkus"; + return PROP_QUARKUS; } @Override @@ -83,7 +86,7 @@ public DetectorResult detect(DetectorContext ctx) { nodes.add(makeNode("quarkus:" + ctx.filePath() + ":quarkus_test:" + lineno, NodeKind.CLASS, "@QuarkusTest " + (className != null ? className : "unknown"), className, lineno, ctx, List.of("@QuarkusTest"), - Map.of("framework", "quarkus", "test", true))); + Map.of(PROP_FRAMEWORK, PROP_QUARKUS, "test", true))); } Matcher cpm = CONFIG_PROPERTY_RE.matcher(lines[i]); @@ -92,7 +95,7 @@ public DetectorResult detect(DetectorContext ctx) { nodes.add(makeNode("quarkus:" + ctx.filePath() + ":config_property:" + lineno, NodeKind.CONFIG_KEY, "@ConfigProperty(" + configKey + ")", configKey, lineno, ctx, List.of("@ConfigProperty"), - Map.of("framework", "quarkus", "config_key", configKey))); + Map.of(PROP_FRAMEWORK, PROP_QUARKUS, "config_key", configKey))); } Matcher cdim = CDI_SCOPE_RE.matcher(lines[i]); @@ -102,7 +105,7 @@ public DetectorResult detect(DetectorContext ctx) { NodeKind.MIDDLEWARE, "@" + annotation + " (CDI)", className != null ? className + "." + annotation : annotation, lineno, ctx, List.of("@" + annotation), - Map.of("framework", "quarkus", "cdi_scope", annotation))); + Map.of(PROP_FRAMEWORK, PROP_QUARKUS, "cdi_scope", annotation))); } Matcher sm = SCHEDULED_RE.matcher(lines[i]); @@ -112,7 +115,7 @@ public DetectorResult detect(DetectorContext ctx) { NodeKind.EVENT, "@Scheduled(" + scheduleExpr + ")", className != null ? className + ".scheduled" : "scheduled", lineno, ctx, List.of("@Scheduled"), - Map.of("framework", "quarkus", "schedule", scheduleExpr))); + Map.of(PROP_FRAMEWORK, PROP_QUARKUS, "schedule", scheduleExpr))); } if (TRANSACTIONAL_RE.matcher(lines[i]).find()) { @@ -120,14 +123,14 @@ public DetectorResult detect(DetectorContext ctx) { NodeKind.MIDDLEWARE, "@Transactional", className != null ? className + ".transactional" : "transactional", lineno, ctx, List.of("@Transactional"), - Map.of("framework", "quarkus"))); + Map.of(PROP_FRAMEWORK, PROP_QUARKUS))); } if (STARTUP_RE.matcher(lines[i]).find()) { nodes.add(makeNode("quarkus:" + ctx.filePath() + ":startup:" + lineno, NodeKind.MIDDLEWARE, "@Startup " + (className != null ? className : "unknown"), className, lineno, ctx, List.of("@Startup"), - Map.of("framework", "quarkus"))); + Map.of(PROP_FRAMEWORK, PROP_QUARKUS))); } } 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 a32cc676..688cafd3 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 @@ -27,6 +27,10 @@ ) @Component public class RabbitmqDetector extends AbstractJavaMessagingDetector { + private static final String PROP_BROKER = "broker"; + private static final String PROP_EXCHANGE = "exchange"; + private static final String PROP_RABBITMQ = "rabbitmq"; + private static final Pattern RABBIT_LISTENER_RE = Pattern.compile( "@RabbitListener\\s*\\(\\s*(?:.*?queues?\\s*=\\s*)?[\\{\"]?\\s*\"([^\"]+)\""); @@ -38,7 +42,7 @@ public class RabbitmqDetector extends AbstractJavaMessagingDetector { @Override public String getName() { - return "rabbitmq"; + return PROP_RABBITMQ; } @Override @@ -82,7 +86,7 @@ public DetectorResult detect(DetectorContext ctx) { if (!m.find()) continue; String exchangeOrQueue = m.group(1); Map props = new LinkedHashMap<>(); - props.put("exchange", exchangeOrQueue); + props.put(PROP_EXCHANGE, exchangeOrQueue); Matcher rk = ROUTING_KEY_RE.matcher(lines[i]); if (rk.find()) props.put("routing_key", rk.group(1)); @@ -93,8 +97,8 @@ public DetectorResult detect(DetectorContext ctx) { node.setId(queueId); node.setKind(NodeKind.QUEUE); node.setLabel("rabbitmq:" + exchangeOrQueue); - node.getProperties().put("broker", "rabbitmq"); - node.getProperties().put("exchange", exchangeOrQueue); + node.getProperties().put(PROP_BROKER, PROP_RABBITMQ); + node.getProperties().put(PROP_EXCHANGE, exchangeOrQueue); nodes.add(node); } @@ -114,8 +118,8 @@ public DetectorResult detect(DetectorContext ctx) { node.setLabel("rabbitmq:exchange:" + exchangeName); node.setFilePath(ctx.filePath()); node.setLineStart(lineNum); - node.getProperties().put("broker", "rabbitmq"); - node.getProperties().put("exchange", exchangeName); + node.getProperties().put(PROP_BROKER, PROP_RABBITMQ); + node.getProperties().put(PROP_EXCHANGE, exchangeName); nodes.add(node); } } @@ -131,7 +135,7 @@ private String ensureQueueNode(String queue, Set seen, List no node.setId(queueId); node.setKind(NodeKind.QUEUE); node.setLabel("rabbitmq:" + queue); - node.getProperties().put("broker", "rabbitmq"); + node.getProperties().put(PROP_BROKER, PROP_RABBITMQ); node.getProperties().put("queue", queue); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/SpringRestDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/SpringRestDetector.java index 6c858273..824ee90a 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/SpringRestDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/SpringRestDetector.java @@ -34,6 +34,11 @@ ) @Component public class SpringRestDetector extends AbstractJavaParserDetector { + private static final String PROP_REQUESTMAPPING = "RequestMapping"; + private static final String PROP_CONSUMES = "consumes"; + private static final String PROP_PATH = "path"; + private static final String PROP_PRODUCES = "produces"; + // ---- Regex fallback patterns ---- private static final Pattern MAPPING_RE = Pattern.compile( @@ -134,8 +139,8 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { if (httpMethod == null) continue; String path = extractAnnotationPath(ann); - String produces = extractAnnotationAttr(ann, "produces"); - String consumes = extractAnnotationAttr(ann, "consumes"); + String produces = extractAnnotationAttr(ann, PROP_PRODUCES); + String consumes = extractAnnotationAttr(ann, PROP_CONSUMES); String fullPath; if (path != null && !path.isEmpty()) { @@ -163,10 +168,10 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { node.getAnnotations().add("@" + annName); node.getProperties().put("framework", "spring_boot"); node.getProperties().put("http_method", httpMethod); - node.getProperties().put("path", fullPath); + node.getProperties().put(PROP_PATH, fullPath); node.getProperties().put("method", methodName); - if (produces != null) node.getProperties().put("produces", produces); - if (consumes != null) node.getProperties().put("consumes", consumes); + if (produces != null) node.getProperties().put(PROP_PRODUCES, produces); + if (consumes != null) node.getProperties().put(PROP_CONSUMES, consumes); // Extract parameter annotations List> params = new ArrayList<>(); @@ -373,9 +378,9 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { node.getAnnotations().add("@" + annotationName); node.getProperties().put("framework", "spring_boot"); node.getProperties().put("http_method", httpMethod); - node.getProperties().put("path", fullPath); - if (produces != null) node.getProperties().put("produces", produces); - if (consumes != null) node.getProperties().put("consumes", consumes); + node.getProperties().put(PROP_PATH, fullPath); + if (produces != null) node.getProperties().put(PROP_PRODUCES, produces); + if (consumes != null) node.getProperties().put(PROP_CONSUMES, consumes); nodes.add(node); CodeEdge edge = new CodeEdge(); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/SpringSecurityDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/SpringSecurityDetector.java index efecfeb7..342290dc 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/SpringSecurityDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/SpringSecurityDetector.java @@ -30,6 +30,11 @@ ) @Component public class SpringSecurityDetector extends AbstractJavaParserDetector { + private static final String PROP_AUTH_REQUIRED = "auth_required"; + private static final String PROP_AUTH_TYPE = "auth_type"; + private static final String PROP_ROLES = "roles"; + private static final String PROP_SPRING_SECURITY = "spring_security"; + // ---- Regex fallback patterns ---- private static final Pattern SECURED_RE = Pattern.compile( @@ -51,7 +56,7 @@ public class SpringSecurityDetector extends AbstractJavaParserDetector { @Override public String getName() { - return "spring_security"; + return PROP_SPRING_SECURITY; } @Override @@ -85,11 +90,11 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { if ("EnableWebSecurity".equals(annName)) { nodes.add(guardNode("auth:" + ctx.filePath() + ":EnableWebSecurity:" + line, "@EnableWebSecurity", line, ctx, List.of("@EnableWebSecurity"), - Map.of("auth_type", "spring_security", "roles", List.of(), "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, List.of(), PROP_AUTH_REQUIRED, true))); } else if ("EnableMethodSecurity".equals(annName)) { nodes.add(guardNode("auth:" + ctx.filePath() + ":EnableMethodSecurity:" + line, "@EnableMethodSecurity", line, ctx, List.of("@EnableMethodSecurity"), - Map.of("auth_type", "spring_security", "roles", List.of(), "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, List.of(), PROP_AUTH_REQUIRED, true))); } } @@ -102,7 +107,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { String methodName = method.getNameAsString(); nodes.add(guardNode("auth:" + ctx.filePath() + ":SecurityFilterChain:" + methodLine, "SecurityFilterChain:" + methodName, methodLine, ctx, List.of(), - Map.of("auth_type", "spring_security", "roles", List.of(), "method_name", methodName, "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, List.of(), "method_name", methodName, PROP_AUTH_REQUIRED, true))); } for (AnnotationExpr ann : method.getAnnotations()) { @@ -113,22 +118,22 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { List roles = extractRolesFromAstAnnotation(ann); nodes.add(guardNode("auth:" + ctx.filePath() + ":Secured:" + line, "@Secured", line, ctx, List.of("@Secured"), - Map.of("auth_type", "spring_security", "roles", roles, "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, roles, PROP_AUTH_REQUIRED, true))); } else if ("PreAuthorize".equals(annName)) { String expr = extractAnnotationStringValue(ann); List roles = expr != null ? extractRolesFromSpel(expr) : List.of(); Map props = new LinkedHashMap<>(); - props.put("auth_type", "spring_security"); - props.put("roles", roles); + props.put(PROP_AUTH_TYPE, PROP_SPRING_SECURITY); + props.put(PROP_ROLES, roles); if (expr != null) props.put("expression", expr); - props.put("auth_required", true); + props.put(PROP_AUTH_REQUIRED, true); nodes.add(guardNode("auth:" + ctx.filePath() + ":PreAuthorize:" + line, "@PreAuthorize", line, ctx, List.of("@PreAuthorize"), props)); } else if ("RolesAllowed".equals(annName)) { List roles = extractRolesFromAstAnnotation(ann); nodes.add(guardNode("auth:" + ctx.filePath() + ":RolesAllowed:" + line, "@RolesAllowed", line, ctx, List.of("@RolesAllowed"), - Map.of("auth_type", "spring_security", "roles", roles, "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, roles, PROP_AUTH_REQUIRED, true))); } } } @@ -140,7 +145,7 @@ private DetectorResult detectWithAst(CompilationUnit cu, DetectorContext ctx) { int line = findLineNumber(text, m.start()); nodes.add(guardNode("auth:" + ctx.filePath() + ":authorizeHttpRequests:" + line, ".authorizeHttpRequests()", line, ctx, List.of(), - Map.of("auth_type", "spring_security", "roles", List.of(), "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, List.of(), PROP_AUTH_REQUIRED, true))); } return DetectorResult.of(nodes, List.of()); @@ -199,7 +204,7 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { List roles = extractRolesFromAnnotation(m.group(1), m.group(2)); nodes.add(guardNode("auth:" + ctx.filePath() + ":Secured:" + line, "@Secured", line, ctx, List.of("@Secured"), - Map.of("auth_type", "spring_security", "roles", roles, "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, roles, PROP_AUTH_REQUIRED, true))); } for (Matcher m = PRE_AUTHORIZE_RE.matcher(text); m.find(); ) { @@ -207,10 +212,10 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { String expr = m.group(1); List roles = extractRolesFromSpel(expr); Map props = new LinkedHashMap<>(); - props.put("auth_type", "spring_security"); - props.put("roles", roles); + props.put(PROP_AUTH_TYPE, PROP_SPRING_SECURITY); + props.put(PROP_ROLES, roles); props.put("expression", expr); - props.put("auth_required", true); + props.put(PROP_AUTH_REQUIRED, true); nodes.add(guardNode("auth:" + ctx.filePath() + ":PreAuthorize:" + line, "@PreAuthorize", line, ctx, List.of("@PreAuthorize"), props)); } @@ -220,21 +225,21 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { List roles = extractRolesFromAnnotation(m.group(1), m.group(2)); nodes.add(guardNode("auth:" + ctx.filePath() + ":RolesAllowed:" + line, "@RolesAllowed", line, ctx, List.of("@RolesAllowed"), - Map.of("auth_type", "spring_security", "roles", roles, "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, roles, PROP_AUTH_REQUIRED, true))); } for (Matcher m = ENABLE_WEB_SECURITY_RE.matcher(text); m.find(); ) { int line = findLineNumber(text, m.start()); nodes.add(guardNode("auth:" + ctx.filePath() + ":EnableWebSecurity:" + line, "@EnableWebSecurity", line, ctx, List.of("@EnableWebSecurity"), - Map.of("auth_type", "spring_security", "roles", List.of(), "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, List.of(), PROP_AUTH_REQUIRED, true))); } for (Matcher m = ENABLE_METHOD_SECURITY_RE.matcher(text); m.find(); ) { int line = findLineNumber(text, m.start()); nodes.add(guardNode("auth:" + ctx.filePath() + ":EnableMethodSecurity:" + line, "@EnableMethodSecurity", line, ctx, List.of("@EnableMethodSecurity"), - Map.of("auth_type", "spring_security", "roles", List.of(), "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, List.of(), PROP_AUTH_REQUIRED, true))); } for (Matcher m = SECURITY_FILTER_CHAIN_RE.matcher(text); m.find(); ) { @@ -242,14 +247,14 @@ private DetectorResult detectWithRegex(DetectorContext ctx) { String methodName = m.group(1); nodes.add(guardNode("auth:" + ctx.filePath() + ":SecurityFilterChain:" + line, "SecurityFilterChain:" + methodName, line, ctx, List.of(), - Map.of("auth_type", "spring_security", "roles", List.of(), "method_name", methodName, "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, List.of(), "method_name", methodName, PROP_AUTH_REQUIRED, true))); } for (Matcher m = AUTHORIZE_HTTP_REQUESTS_RE.matcher(text); m.find(); ) { int line = findLineNumber(text, m.start()); nodes.add(guardNode("auth:" + ctx.filePath() + ":authorizeHttpRequests:" + line, ".authorizeHttpRequests()", line, ctx, List.of(), - Map.of("auth_type", "spring_security", "roles", List.of(), "auth_required", true))); + Map.of(PROP_AUTH_TYPE, PROP_SPRING_SECURITY, PROP_ROLES, List.of(), PROP_AUTH_REQUIRED, true))); } return DetectorResult.of(nodes, List.of()); 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 475523b6..07512504 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 @@ -27,6 +27,11 @@ ) @Component public class TibcoEmsDetector extends AbstractJavaMessagingDetector { + private static final String PROP_BROKER = "broker"; + private static final String PROP_QUEUE = "queue"; + private static final String PROP_TIBCO_EMS = "tibco_ems"; + private static final String PROP_TOPIC = "topic"; + private static final Pattern TIBJMS_FACTORY_RE = Pattern.compile( "\\b(TibjmsConnectionFactory|TibjmsQueueConnectionFactory|TibjmsTopicConnectionFactory)\\b"); @@ -44,7 +49,7 @@ public class TibcoEmsDetector extends AbstractJavaMessagingDetector { @Override public String getName() { - return "tibco_ems"; + return PROP_TIBCO_EMS; } @Override @@ -94,7 +99,7 @@ public DetectorResult detect(DetectorContext ctx) { node.setId(nodeId); node.setKind(NodeKind.MESSAGE_QUEUE); node.setLabel("ems:" + factoryType); - node.getProperties().put("broker", "tibco_ems"); + node.getProperties().put(PROP_BROKER, PROP_TIBCO_EMS); node.getProperties().put("factory_type", factoryType); if (!serverUrls.isEmpty()) node.getProperties().put("server_url", serverUrls.get(0)); nodes.add(node); @@ -116,18 +121,18 @@ public DetectorResult detect(DetectorContext ctx) { String queueName = m.group(1); String queueId = ensureQueueNode(queueName, seenQueues, nodes); if (isProducer) addMessagingEdge(classNodeId, queueId, EdgeKind.SENDS_TO, - className + " sends to " + queueName, Map.of("queue", queueName), edges); + className + " sends to " + queueName, Map.of(PROP_QUEUE, queueName), edges); if (isConsumer) addMessagingEdge(classNodeId, queueId, EdgeKind.RECEIVES_FROM, - className + " receives from " + queueName, Map.of("queue", queueName), edges); + className + " receives from " + queueName, Map.of(PROP_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) addMessagingEdge(classNodeId, topicId, EdgeKind.SENDS_TO, - className + " sends to " + topicName, Map.of("topic", topicName), edges); + className + " sends to " + topicName, Map.of(PROP_TOPIC, topicName), edges); if (isConsumer) addMessagingEdge(classNodeId, topicId, EdgeKind.RECEIVES_FROM, - className + " receives from " + topicName, Map.of("topic", topicName), edges); + className + " receives from " + topicName, Map.of(PROP_TOPIC, topicName), edges); } } @@ -150,8 +155,8 @@ private String ensureQueueNode(String name, Set seen, List nod node.setId(id); node.setKind(NodeKind.QUEUE); node.setLabel("ems:queue:" + name); - node.getProperties().put("broker", "tibco_ems"); - node.getProperties().put("queue", name); + node.getProperties().put(PROP_BROKER, PROP_TIBCO_EMS); + node.getProperties().put(PROP_QUEUE, name); nodes.add(node); } return id; @@ -165,8 +170,8 @@ private String ensureTopicNode(String name, Set seen, List nod node.setId(id); node.setKind(NodeKind.TOPIC); node.setLabel("ems:topic:" + name); - node.getProperties().put("broker", "tibco_ems"); - node.getProperties().put("topic", name); + node.getProperties().put(PROP_BROKER, PROP_TIBCO_EMS); + node.getProperties().put(PROP_TOPIC, name); nodes.add(node); } return id; diff --git a/src/main/java/io/github/randomcodespace/iq/detector/java/WebSocketDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/java/WebSocketDetector.java index 4982bd25..562775c2 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/java/WebSocketDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/java/WebSocketDetector.java @@ -28,6 +28,11 @@ ) @Component public class WebSocketDetector extends AbstractRegexDetector { + private static final String PROP_DESTINATION = "destination"; + private static final String PROP_PROTOCOL = "protocol"; + private static final String PROP_TYPE = "type"; + private static final String PROP_WEBSOCKET = "websocket"; + private static final Pattern CLASS_RE = Pattern.compile("(?:public\\s+)?class\\s+(\\w+)"); private static final Pattern SERVER_ENDPOINT_RE = Pattern.compile("@ServerEndpoint\\s*\\(\\s*(?:value\\s*=\\s*)?\"([^\"]+)\""); @@ -42,7 +47,7 @@ public class WebSocketDetector extends AbstractRegexDetector { @Override public String getName() { - return "websocket"; + return PROP_WEBSOCKET; } @Override @@ -89,8 +94,8 @@ public DetectorResult detect(DetectorContext ctx) { node.setLineStart(lineNum); node.getAnnotations().add("@ServerEndpoint"); node.getProperties().put("path", path); - node.getProperties().put("protocol", "websocket"); - node.getProperties().put("type", "jsr356"); + node.getProperties().put(PROP_PROTOCOL, PROP_WEBSOCKET); + node.getProperties().put(PROP_TYPE, "jsr356"); nodes.add(node); CodeEdge edge = new CodeEdge(); @@ -120,9 +125,9 @@ public DetectorResult detect(DetectorContext ctx) { node.setFilePath(ctx.filePath()); node.setLineStart(i + 1); node.getAnnotations().add("@MessageMapping"); - node.getProperties().put("destination", destination); - node.getProperties().put("protocol", "websocket"); - node.getProperties().put("type", "stomp"); + node.getProperties().put(PROP_DESTINATION, destination); + node.getProperties().put(PROP_PROTOCOL, PROP_WEBSOCKET); + node.getProperties().put(PROP_TYPE, "stomp"); nodes.add(node); CodeEdge edge = new CodeEdge(); @@ -143,8 +148,8 @@ public DetectorResult detect(DetectorContext ctx) { sendNode.setId(sendId); sendNode.setKind(NodeKind.WEBSOCKET_ENDPOINT); sendNode.setLabel("WS TOPIC " + sendDest); - sendNode.getProperties().put("destination", sendDest); - sendNode.getProperties().put("protocol", "websocket"); + sendNode.getProperties().put(PROP_DESTINATION, sendDest); + sendNode.getProperties().put(PROP_PROTOCOL, PROP_WEBSOCKET); nodes.add(sendNode); CodeEdge sendEdge = new CodeEdge(); @@ -169,8 +174,8 @@ public DetectorResult detect(DetectorContext ctx) { node.setFilePath(ctx.filePath()); node.setLineStart(lineNum); node.getProperties().put("path", path); - node.getProperties().put("protocol", "stomp"); - node.getProperties().put("type", "stomp_endpoint"); + node.getProperties().put(PROP_PROTOCOL, "stomp"); + node.getProperties().put(PROP_TYPE, "stomp_endpoint"); nodes.add(node); } @@ -182,7 +187,7 @@ public DetectorResult detect(DetectorContext ctx) { edge.setKind(EdgeKind.PRODUCES); edge.setSourceId(classNodeId); edge.setTarget(new CodeNode("ws:topic:" + destination, NodeKind.WEBSOCKET_ENDPOINT, destination)); - edge.setProperties(Map.of("destination", destination)); + edge.setProperties(Map.of(PROP_DESTINATION, destination)); edges.add(edge); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/kotlin/KtorRouteDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/kotlin/KtorRouteDetector.java index b8fc3214..f26b13d1 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/kotlin/KtorRouteDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/kotlin/KtorRouteDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.kotlin; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeNode; @@ -29,6 +28,9 @@ ) @Component public class KtorRouteDetector extends AbstractAntlrDetector { + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_KTOR = "ktor"; + private static final Pattern ENDPOINT_PATTERN = Pattern.compile("\\b(get|post|put|delete|patch)\\(\\s*\"([^\"]+)\"\\s*\\)\\s*\\{"); private static final Pattern ROUTING_PATTERN = Pattern.compile("\\brouting\\s*\\{"); @@ -99,7 +101,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { CodeNode n = new CodeNode(); n.setId("ktor:" + fp + ":routing:" + line); n.setKind(NodeKind.MODULE); n.setLabel("routing"); n.setFqn(fp + "::routing"); n.setFilePath(fp); n.setLineStart(line); - n.getProperties().put("framework", "ktor"); n.getProperties().put("type", "router"); + n.getProperties().put(PROP_FRAMEWORK, PROP_KTOR); n.getProperties().put("type", "router"); nodes.add(n); } @@ -114,7 +116,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { n.setKind(NodeKind.ENDPOINT); n.setLabel(method + " " + path); n.setFqn(fp + "::" + method + ":" + path); n.setFilePath(fp); n.setLineStart(line); n.getProperties().put("protocol", "REST"); n.getProperties().put("http_method", method); - n.getProperties().put("path_pattern", path); n.getProperties().put("framework", "ktor"); + n.getProperties().put("path_pattern", path); n.getProperties().put(PROP_FRAMEWORK, PROP_KTOR); nodes.add(n); } @@ -125,7 +127,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { CodeNode n = new CodeNode(); n.setId("ktor:" + fp + ":install:" + feature + ":" + line); n.setKind(NodeKind.MIDDLEWARE); n.setLabel("install:" + feature); n.setFqn(fp + "::install:" + feature); n.setFilePath(fp); n.setLineStart(line); - n.getProperties().put("framework", "ktor"); n.getProperties().put("feature", feature); + n.getProperties().put(PROP_FRAMEWORK, PROP_KTOR); n.getProperties().put("feature", feature); nodes.add(n); } @@ -136,7 +138,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { CodeNode n = new CodeNode(); n.setId("ktor:" + fp + ":auth:" + authName + ":" + line); n.setKind(NodeKind.GUARD); n.setLabel("authenticate:" + authName); n.setFqn(fp + "::authenticate:" + authName); n.setFilePath(fp); n.setLineStart(line); - n.getProperties().put("framework", "ktor"); n.getProperties().put("auth_name", authName); + n.getProperties().put(PROP_FRAMEWORK, PROP_KTOR); n.getProperties().put("auth_name", authName); nodes.add(n); } 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 e3775ee2..fed53143 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 @@ -60,7 +60,6 @@ protected DetectorResult detectWithAst(ParseTree tree, DetectorContext ctx) { List edges = new ArrayList<>(); String filePath = ctx.filePath(); String moduleName = ctx.moduleName(); - String text = ctx.content(); // Walk for decorated functions (task definitions) ParseTreeWalker.DEFAULT.walk(new Python3ParserBaseListener() { 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 5ed00883..816fcee5 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 @@ -29,6 +29,11 @@ ) @Component public class DjangoAuthDetector extends AbstractPythonAntlrDetector { + private static final String PROP_AUTH_REQUIRED = "auth_required"; + private static final String PROP_AUTH_TYPE = "auth_type"; + private static final String PROP_DJANGO = "django"; + private static final String PROP_PERMISSIONS = "permissions"; + // --- Regex fallback patterns --- private static final Pattern LOGIN_REQUIRED_RE = Pattern.compile("@login_required\\b"); @@ -153,9 +158,9 @@ private static CodeNode createLoginRequiredGuard(String filePath, String moduleN 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); + node.getProperties().put(PROP_AUTH_TYPE, PROP_DJANGO); + node.getProperties().put(PROP_PERMISSIONS, List.of()); + node.getProperties().put(PROP_AUTH_REQUIRED, true); return node; } @@ -168,9 +173,9 @@ private static CodeNode createPermissionRequiredGuard(String filePath, String mo 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); + node.getProperties().put(PROP_AUTH_TYPE, PROP_DJANGO); + node.getProperties().put(PROP_PERMISSIONS, List.of(permission)); + node.getProperties().put(PROP_AUTH_REQUIRED, true); return node; } @@ -183,10 +188,10 @@ private static CodeNode createUserPassesTestGuard(String filePath, String module 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(PROP_AUTH_TYPE, PROP_DJANGO); + node.getProperties().put(PROP_PERMISSIONS, List.of()); node.getProperties().put("test_function", testFunc); - node.getProperties().put("auth_required", true); + node.getProperties().put(PROP_AUTH_REQUIRED, true); return node; } @@ -199,11 +204,11 @@ private static CodeNode createMixinGuard(String filePath, String moduleName, int 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(PROP_AUTH_TYPE, PROP_DJANGO); + node.getProperties().put(PROP_PERMISSIONS, List.of()); node.getProperties().put("mixin", mixin); node.getProperties().put("class_name", className); - node.getProperties().put("auth_required", true); + node.getProperties().put(PROP_AUTH_REQUIRED, true); 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 d51fb3e8..70e62a95 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 @@ -28,6 +28,12 @@ ) @Component public class FastAPIAuthDetector extends AbstractPythonAntlrDetector { + private static final String PROP_AUTH_FLOW = "auth_flow"; + private static final String PROP_AUTH_REQUIRED = "auth_required"; + private static final String PROP_AUTH_TYPE = "auth_type"; + private static final String PROP_FASTAPI = "fastapi"; + private static final String PROP_OAUTH2 = "oauth2"; + // --- Regex fallback patterns --- private static final Pattern DEPENDS_AUTH_RE = Pattern.compile( @@ -143,10 +149,10 @@ private static CodeNode createDependsGuard(String filePath, String moduleName, i 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(PROP_AUTH_TYPE, PROP_FASTAPI); + node.getProperties().put(PROP_AUTH_FLOW, PROP_OAUTH2); node.getProperties().put("dependency", depName); - node.getProperties().put("auth_required", true); + node.getProperties().put(PROP_AUTH_REQUIRED, true); return node; } @@ -159,10 +165,10 @@ private static CodeNode createSecurityGuard(String filePath, String 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(PROP_AUTH_TYPE, PROP_FASTAPI); + node.getProperties().put(PROP_AUTH_FLOW, PROP_OAUTH2); node.getProperties().put("scheme", schemeName); - node.getProperties().put("auth_required", true); + node.getProperties().put(PROP_AUTH_REQUIRED, true); return node; } @@ -175,9 +181,9 @@ private static CodeNode createHttpBearerGuard(String filePath, String 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); + node.getProperties().put(PROP_AUTH_TYPE, PROP_FASTAPI); + node.getProperties().put(PROP_AUTH_FLOW, "bearer"); + node.getProperties().put(PROP_AUTH_REQUIRED, true); return node; } @@ -190,10 +196,10 @@ private static CodeNode createOAuth2PasswordBearerGuard(String filePath, String 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(PROP_AUTH_TYPE, PROP_FASTAPI); + node.getProperties().put(PROP_AUTH_FLOW, PROP_OAUTH2); node.getProperties().put("token_url", tokenUrl); - node.getProperties().put("auth_required", true); + node.getProperties().put(PROP_AUTH_REQUIRED, true); return node; } @@ -206,9 +212,9 @@ private static CodeNode createHttpBasicGuard(String filePath, String 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); + node.getProperties().put(PROP_AUTH_TYPE, PROP_FASTAPI); + node.getProperties().put(PROP_AUTH_FLOW, "basic"); + node.getProperties().put(PROP_AUTH_REQUIRED, true); return node; } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/python/KafkaPythonDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/python/KafkaPythonDetector.java index 43b9d788..ab809f44 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/python/KafkaPythonDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/python/KafkaPythonDetector.java @@ -30,6 +30,10 @@ ) @Component public class KafkaPythonDetector extends AbstractAntlrDetector { + private static final String PROP_PRODUCER = "producer"; + private static final String PROP_ROLE = "role"; + private static final String PROP_TOPIC = "topic"; + // --- Regex patterns --- private static final Pattern PRODUCER_RE = Pattern.compile( @@ -117,7 +121,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(fp); node.setLineStart(lineno); - node.getProperties().put("role", "producer"); + node.getProperties().put(PROP_ROLE, PROP_PRODUCER); nodes.add(node); } } @@ -132,7 +136,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(fp); node.setLineStart(lineno); - node.getProperties().put("role", "consumer"); + node.getProperties().put(PROP_ROLE, "consumer"); nodes.add(node); } } @@ -142,24 +146,24 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { Matcher sm = SEND_RE.matcher(lines[i]); if (sm.find() && lines[i].contains("send")) { String topic = sm.group(1); - String topicId = ensureTopic(nodes, seenTopics, fp, moduleName, topic, "producer", lineno); + String topicId = ensureTopic(nodes, seenTopics, fp, moduleName, topic, PROP_PRODUCER, lineno); CodeEdge edge = new CodeEdge(); edge.setId(fileNodeId + "->produces->" + topicId); edge.setKind(EdgeKind.PRODUCES); edge.setSourceId(fileNodeId); - edge.getProperties().put("topic", topic); + edge.getProperties().put(PROP_TOPIC, topic); edges.add(edge); continue; } Matcher pm = PRODUCE_RE.matcher(lines[i]); if (pm.find()) { String topic = pm.group(1); - String topicId = ensureTopic(nodes, seenTopics, fp, moduleName, topic, "producer", lineno); + String topicId = ensureTopic(nodes, seenTopics, fp, moduleName, topic, PROP_PRODUCER, lineno); CodeEdge edge = new CodeEdge(); edge.setId(fileNodeId + "->produces->" + topicId); edge.setKind(EdgeKind.PRODUCES); edge.setSourceId(fileNodeId); - edge.getProperties().put("topic", topic); + edge.getProperties().put(PROP_TOPIC, topic); edges.add(edge); } } @@ -174,7 +178,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { edge.setId(fileNodeId + "->consumes->" + topicId); edge.setKind(EdgeKind.CONSUMES); edge.setSourceId(fileNodeId); - edge.getProperties().put("topic", topic); + edge.getProperties().put(PROP_TOPIC, topic); edges.add(edge); } } @@ -209,8 +213,8 @@ private String ensureTopic(List nodes, Set seenTopics, node.setFilePath(fp); node.setLineStart(lineno); node.getProperties().put("broker", "kafka"); - node.getProperties().put("topic", topic); - node.getProperties().put("role", role); + node.getProperties().put(PROP_TOPIC, topic); + node.getProperties().put(PROP_ROLE, role); nodes.add(node); } return topicId; 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 4a03d135..117dd77c 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 @@ -8,7 +8,6 @@ import io.github.randomcodespace.iq.model.CodeNode; import io.github.randomcodespace.iq.model.EdgeKind; import io.github.randomcodespace.iq.model.NodeKind; -import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.springframework.stereotype.Component; @@ -33,6 +32,8 @@ ) @Component public class PythonStructuresDetector extends AbstractPythonAntlrDetector { + private static final String PROP_EXPORTED = "exported"; + // --- Regex patterns (for fallback) --- private static final Pattern CLASS_RE = Pattern.compile( @@ -114,7 +115,7 @@ public void enterFuncdef(Python3Parser.FuncdefContext funcCtx) { properties.put("async", true); } if (allInfo.exports != null && allInfo.exports.contains(funcName)) { - properties.put("exported", true); + properties.put(PROP_EXPORTED, true); } if (indent == 0) { @@ -148,12 +149,12 @@ public void enterImport_from(Python3Parser.Import_fromContext importCtx) { // from os.path import join StringBuilder fromModule = new StringBuilder(); if (importCtx.DOT() != null) { - for (var dot : importCtx.DOT()) { + for (var ignored : importCtx.DOT()) { fromModule.append("."); } } if (importCtx.ELLIPSIS() != null) { - for (var ellipsis : importCtx.ELLIPSIS()) { + for (var ignored : importCtx.ELLIPSIS()) { fromModule.append("..."); } } @@ -228,7 +229,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { properties.put("async", true); } if (allInfo.exports != null && allInfo.exports.contains(funcName)) { - properties.put("exported", true); + properties.put(PROP_EXPORTED, true); } if (indentLen == 0) { @@ -301,7 +302,7 @@ private static Map buildBaseProperties(String basesStr, String c properties.put("bases", bases); } if (allExports != null && allExports.contains(className)) { - properties.put("exported", true); + properties.put(PROP_EXPORTED, true); } return properties; } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/rust/ActixWebDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/rust/ActixWebDetector.java index 2193c23d..fb83e591 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/rust/ActixWebDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/rust/ActixWebDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.rust; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeNode; @@ -27,6 +26,12 @@ ) @Component public class ActixWebDetector extends AbstractAntlrDetector { + private static final String PROP_ACTIX_WEB = "actix_web"; + private static final String PROP_AXUM = "axum"; + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_HTTP_METHOD = "http_method"; + private static final String PROP_PATH = "path"; + private static final Pattern ACTIX_ATTR_RE = Pattern.compile("#\\[(get|post|put|delete)\\s*\\(\\s*\"([^\"]*)\"\\s*\\)\\s*\\]"); private static final Pattern HTTP_SERVER_RE = Pattern.compile("HttpServer::new\\s*\\("); @@ -38,7 +43,7 @@ public class ActixWebDetector extends AbstractAntlrDetector { private static final Pattern FN_RE = Pattern.compile("(?:pub\\s+)?(?:async\\s+)?fn\\s+(\\w+)"); @Override - public String getName() { return "actix_web"; } + public String getName() { return PROP_ACTIX_WEB; } @Override public Set getSupportedLanguages() { return Set.of("rust"); } @@ -55,7 +60,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { if (text == null || text.isEmpty()) return DetectorResult.empty(); boolean hasMarker = false; - for (String marker : List.of("#[get", "#[post", "#[put", "#[delete", "HttpServer::new", "web::get", "web::post", "web::resource", "Router::new", ".layer(", "actix_web::main", "tokio::main", "actix_web", "axum")) { + for (String marker : List.of("#[get", "#[post", "#[put", "#[delete", "HttpServer::new", "web::get", "web::post", "web::resource", "Router::new", ".layer(", "actix_web::main", "tokio::main", PROP_ACTIX_WEB, PROP_AXUM)) { if (text.contains(marker)) { hasMarker = true; break; } } if (!hasMarker) return DetectorResult.empty(); @@ -84,9 +89,9 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(fnName); node.setFilePath(filePath); node.setLineStart(lineno); - node.getProperties().put("framework", "actix_web"); - node.getProperties().put("http_method", method); - node.getProperties().put("path", path); + node.getProperties().put(PROP_FRAMEWORK, PROP_ACTIX_WEB); + node.getProperties().put(PROP_HTTP_METHOD, method); + node.getProperties().put(PROP_PATH, path); nodes.add(node); } @@ -99,7 +104,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn("HttpServer"); node.setFilePath(filePath); node.setLineStart(lineno); - node.getProperties().put("framework", "actix_web"); + node.getProperties().put(PROP_FRAMEWORK, PROP_ACTIX_WEB); nodes.add(node); } @@ -115,9 +120,9 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(handler); node.setFilePath(filePath); node.setLineStart(lineno); - node.getProperties().put("framework", "actix_web"); - node.getProperties().put("http_method", method); - node.getProperties().put("path", path); + node.getProperties().put(PROP_FRAMEWORK, PROP_ACTIX_WEB); + node.getProperties().put(PROP_HTTP_METHOD, method); + node.getProperties().put(PROP_PATH, path); node.getProperties().put("handler", handler); nodes.add(node); } @@ -132,8 +137,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(path); node.setFilePath(filePath); node.setLineStart(lineno); - node.getProperties().put("framework", "actix_web"); - node.getProperties().put("path", path); + node.getProperties().put(PROP_FRAMEWORK, PROP_ACTIX_WEB); + node.getProperties().put(PROP_PATH, path); nodes.add(node); } @@ -149,9 +154,9 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(handler); node.setFilePath(filePath); node.setLineStart(lineno); - node.getProperties().put("framework", "axum"); - node.getProperties().put("http_method", method); - node.getProperties().put("path", path); + node.getProperties().put(PROP_FRAMEWORK, PROP_AXUM); + node.getProperties().put(PROP_HTTP_METHOD, method); + node.getProperties().put(PROP_PATH, path); node.getProperties().put("handler", handler); nodes.add(node); } @@ -166,7 +171,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn(mwName); node.setFilePath(filePath); node.setLineStart(lineno); - node.getProperties().put("framework", "axum"); + node.getProperties().put(PROP_FRAMEWORK, PROP_AXUM); node.getProperties().put("middleware", mwName); nodes.add(node); } @@ -181,7 +186,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFqn("main"); node.setFilePath(filePath); node.setLineStart(lineno); - node.getProperties().put("framework", attr.contains("actix") ? "actix_web" : "tokio"); + node.getProperties().put(PROP_FRAMEWORK, attr.contains("actix") ? PROP_ACTIX_WEB : "tokio"); nodes.add(node); } } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/rust/RustStructuresDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/rust/RustStructuresDetector.java index 7ff58d6d..a913ecd2 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/rust/RustStructuresDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/rust/RustStructuresDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.rust; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -29,6 +28,8 @@ ) @Component public class RustStructuresDetector extends AbstractAntlrDetector { + private static final String PROP_TYPE = "type"; + private static final Pattern USE_RE = Pattern.compile("^\\s*use\\s+([\\w:]+)", Pattern.MULTILINE); private static final Pattern STRUCT_RE = Pattern.compile("^\\s*(?:pub\\s+)?struct\\s+(\\w+)", Pattern.MULTILINE); @@ -84,7 +85,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { 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())); - n.getProperties().put("type", "struct"); nodes.add(n); + n.getProperties().put(PROP_TYPE, "struct"); nodes.add(n); } m = TRAIT_RE.matcher(text); @@ -93,7 +94,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { 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())); - n.getProperties().put("type", "trait"); nodes.add(n); + n.getProperties().put(PROP_TYPE, "trait"); nodes.add(n); } m = ENUM_RE.matcher(text); @@ -128,7 +129,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { 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())); - n.getProperties().put("type", "function"); nodes.add(n); + n.getProperties().put(PROP_TYPE, "function"); nodes.add(n); } m = MACRO_RE.matcher(text); @@ -137,7 +138,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { CodeNode n = new CodeNode(); n.setId(fp + ":macro:" + name); n.setKind(NodeKind.METHOD); n.setLabel(name + "!"); n.setFqn(name + "!"); n.setFilePath(fp); n.setLineStart(findLineNumber(text, m.start())); - n.getProperties().put("type", "macro"); nodes.add(n); + n.getProperties().put(PROP_TYPE, "macro"); nodes.add(n); } return DetectorResult.of(nodes, edges); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/typescript/FastifyRouteDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/typescript/FastifyRouteDetector.java index a38e9678..2b7bcae0 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/typescript/FastifyRouteDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/typescript/FastifyRouteDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.typescript; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -31,6 +30,9 @@ ) @Component public class FastifyRouteDetector extends AbstractAntlrDetector { + private static final String PROP_FASTIFY = "fastify"; + private static final String PROP_FRAMEWORK = "framework"; + /** * Guard pattern: file must contain a Fastify import/require to be considered. @@ -116,7 +118,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.getProperties().put("protocol", "REST"); node.getProperties().put("http_method", method); node.getProperties().put("path_pattern", path); - node.getProperties().put("framework", "fastify"); + node.getProperties().put(PROP_FRAMEWORK, PROP_FASTIFY); nodes.add(node); } @@ -148,7 +150,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.getProperties().put("protocol", "REST"); node.getProperties().put("http_method", method); node.getProperties().put("path_pattern", path); - node.getProperties().put("framework", "fastify"); + node.getProperties().put(PROP_FRAMEWORK, PROP_FASTIFY); Matcher schemaMatcher = SCHEMA_PATTERN.matcher(routeBlock); if (schemaMatcher.find()) { @@ -170,7 +172,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { edge.setId(edgeSource + "->" + edgeTarget); edge.setKind(EdgeKind.IMPORTS); edge.setSourceId(edgeSource); - edge.getProperties().put("framework", "fastify"); + edge.getProperties().put(PROP_FRAMEWORK, PROP_FASTIFY); edge.getProperties().put("plugin", pluginRef); edges.add(edge); } @@ -190,7 +192,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "fastify"); + node.getProperties().put(PROP_FRAMEWORK, PROP_FASTIFY); node.getProperties().put("hook_name", hookName); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/typescript/KafkaJSDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/typescript/KafkaJSDetector.java index f0d8f18c..b7560aaf 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/typescript/KafkaJSDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/typescript/KafkaJSDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.typescript; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -28,6 +27,9 @@ ) @Component public class KafkaJSDetector extends AbstractAntlrDetector { + private static final String PROP_KAFKA = "kafka"; + private static final String PROP_TOPIC = "topic"; + private static final Pattern KAFKA_NEW_RE = Pattern.compile("new\\s+Kafka\\s*\\(\\s*\\{"); private static final Pattern PRODUCER_RE = Pattern.compile("\\.producer\\s*\\(\\s*\\)"); @@ -68,7 +70,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { String fp = ctx.filePath(); String moduleName = ctx.moduleName(); - if (!text.contains("Kafka") && !text.contains("kafka")) { + if (!text.contains("Kafka") && !text.contains(PROP_KAFKA)) { return DetectorResult.empty(); } @@ -90,7 +92,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(fp); node.setLineStart(lineno); - node.getProperties().put("broker", "kafka"); + node.getProperties().put("broker", PROP_KAFKA); node.getProperties().put("library", "kafkajs"); nodes.add(node); } @@ -117,7 +119,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { edge.setId(fileNodeId + "->produces->" + topicId); edge.setKind(EdgeKind.PRODUCES); edge.setSourceId(fileNodeId); - edge.getProperties().put("topic", topic); + edge.getProperties().put(PROP_TOPIC, topic); edges.add(edge); } @@ -146,7 +148,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { edge.setId(fileNodeId + "->consumes->" + topicId); edge.setKind(EdgeKind.CONSUMES); edge.setSourceId(fileNodeId); - edge.getProperties().put("topic", topic); + edge.getProperties().put(PROP_TOPIC, topic); edges.add(edge); } @@ -185,8 +187,8 @@ private String ensureTopic(List nodes, Set seenTopics, node.setModule(moduleName); node.setFilePath(fp); node.setLineStart(lineno); - node.getProperties().put("broker", "kafka"); - node.getProperties().put("topic", topic); + node.getProperties().put("broker", PROP_KAFKA); + node.getProperties().put(PROP_TOPIC, topic); nodes.add(node); } return topicId; diff --git a/src/main/java/io/github/randomcodespace/iq/detector/typescript/MongooseORMDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/typescript/MongooseORMDetector.java index 74c0fafe..df1a1feb 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/typescript/MongooseORMDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/typescript/MongooseORMDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.typescript; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -28,6 +27,10 @@ ) @Component public class MongooseORMDetector extends AbstractAntlrDetector { + private static final String PROP_DEFINITION = "definition"; + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_MONGOOSE = "mongoose"; + private static final Pattern MODEL_RE = Pattern.compile( "mongoose\\.model\\s*\\(\\s*['\"](\\w+)['\"]" @@ -90,7 +93,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "mongoose"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MONGOOSE); nodes.add(node); } @@ -108,8 +111,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "mongoose"); - node.getProperties().put("definition", "schema"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MONGOOSE); + node.getProperties().put(PROP_DEFINITION, "schema"); nodes.add(node); } @@ -128,8 +131,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "mongoose"); - node.getProperties().put("definition", "model"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MONGOOSE); + node.getProperties().put(PROP_DEFINITION, "model"); nodes.add(node); } @@ -145,7 +148,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { } if (!virtuals.isEmpty()) { for (CodeNode node : nodes) { - if ("schema".equals(node.getProperties().get("definition"))) { + if ("schema".equals(node.getProperties().get(PROP_DEFINITION))) { node.getProperties().put("virtuals", virtuals); } } @@ -168,7 +171,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "mongoose"); + node.getProperties().put(PROP_FRAMEWORK, PROP_MONGOOSE); node.getProperties().put("hook_type", hookType); node.getProperties().put("event", eventName); nodes.add(node); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetector.java index 177509c4..6bc34188 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/typescript/NestJSGuardsDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.typescript; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeNode; @@ -27,6 +26,10 @@ ) @Component public class NestJSGuardsDetector extends AbstractAntlrDetector { + private static final String PROP_AUTH_TYPE = "auth_type"; + private static final String PROP_NESTJS_GUARD = "nestjs_guard"; + private static final String PROP_ROLES = "roles"; + private static final Pattern NESTJS_IMPORT = Pattern.compile("from\\s+['\"]@nestjs/"); @@ -92,9 +95,9 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFilePath(filePath); node.setLineStart(line); node.getAnnotations().add("@UseGuards"); - node.getProperties().put("auth_type", "nestjs_guard"); + node.getProperties().put(PROP_AUTH_TYPE, PROP_NESTJS_GUARD); node.getProperties().put("guard_name", guardName); - node.getProperties().put("roles", List.of()); + node.getProperties().put(PROP_ROLES, List.of()); nodes.add(node); } } @@ -114,8 +117,8 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFilePath(filePath); node.setLineStart(line); node.getAnnotations().add("@Roles"); - node.getProperties().put("auth_type", "nestjs_guard"); - node.getProperties().put("roles", roles); + node.getProperties().put(PROP_AUTH_TYPE, PROP_NESTJS_GUARD); + node.getProperties().put(PROP_ROLES, roles); nodes.add(node); } @@ -132,9 +135,9 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("auth_type", "nestjs_guard"); + node.getProperties().put(PROP_AUTH_TYPE, PROP_NESTJS_GUARD); node.getProperties().put("guard_impl", "canActivate"); - node.getProperties().put("roles", List.of()); + node.getProperties().put(PROP_ROLES, List.of()); nodes.add(node); } @@ -153,9 +156,9 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFilePath(filePath); node.setLineStart(line); node.getAnnotations().add("AuthGuard"); - node.getProperties().put("auth_type", "nestjs_guard"); + node.getProperties().put(PROP_AUTH_TYPE, PROP_NESTJS_GUARD); node.getProperties().put("strategy", strategy); - node.getProperties().put("roles", List.of()); + node.getProperties().put(PROP_ROLES, List.of()); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/typescript/PassportJwtDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/typescript/PassportJwtDetector.java index 4c13148d..4ce5a372 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/typescript/PassportJwtDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/typescript/PassportJwtDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.typescript; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeNode; @@ -27,6 +26,9 @@ ) @Component public class PassportJwtDetector extends AbstractAntlrDetector { + private static final String PROP_AUTH_TYPE = "auth_type"; + private static final String PROP_JWT = "jwt"; + private static final Pattern PASSPORT_USE_PATTERN = Pattern.compile( "passport\\.use\\(\\s*new\\s+(\\w+Strategy)\\s*\\(" @@ -86,7 +88,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("auth_type", "passport"); + node.getProperties().put(PROP_AUTH_TYPE, "passport"); node.getProperties().put("strategy", strategyName); nodes.add(node); } @@ -105,7 +107,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("auth_type", "jwt"); + node.getProperties().put(PROP_AUTH_TYPE, PROP_JWT); node.getProperties().put("strategy", strategy); nodes.add(node); } @@ -123,7 +125,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("auth_type", "jwt"); + node.getProperties().put(PROP_AUTH_TYPE, PROP_JWT); nodes.add(node); } @@ -140,7 +142,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("auth_type", "jwt"); + node.getProperties().put(PROP_AUTH_TYPE, PROP_JWT); node.getProperties().put("library", "express-jwt"); nodes.add(node); } @@ -158,7 +160,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("auth_type", "jwt"); + node.getProperties().put(PROP_AUTH_TYPE, PROP_JWT); node.getProperties().put("library", "express-jwt"); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/typescript/RemixRouteDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/typescript/RemixRouteDetector.java index 2f018185..e73865cd 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/typescript/RemixRouteDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/typescript/RemixRouteDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.typescript; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeNode; @@ -27,6 +26,11 @@ ) @Component public class RemixRouteDetector extends AbstractAntlrDetector { + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_REMIX = "remix"; + private static final String PROP_ROUTE_PATH = "route_path"; + private static final String PROP_TYPE = "type"; + private static final Pattern LOADER_PATTERN = Pattern.compile( "export\\s+(?:async\\s+)?function\\s+loader\\s*\\(" @@ -90,11 +94,11 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "remix"); - node.getProperties().put("type", "loader"); + node.getProperties().put(PROP_FRAMEWORK, PROP_REMIX); + node.getProperties().put(PROP_TYPE, "loader"); node.getProperties().put("http_method", "GET"); if (routePath != null) { - node.getProperties().put("route_path", routePath); + node.getProperties().put(PROP_ROUTE_PATH, routePath); } nodes.add(node); } @@ -112,11 +116,11 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "remix"); - node.getProperties().put("type", "action"); + node.getProperties().put(PROP_FRAMEWORK, PROP_REMIX); + node.getProperties().put(PROP_TYPE, "action"); node.getProperties().put("http_method", "POST"); if (routePath != null) { - node.getProperties().put("route_path", routePath); + node.getProperties().put(PROP_ROUTE_PATH, routePath); } nodes.add(node); } @@ -139,10 +143,10 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "remix"); - node.getProperties().put("type", "component"); + node.getProperties().put(PROP_FRAMEWORK, PROP_REMIX); + node.getProperties().put(PROP_TYPE, "component"); if (routePath != null) { - node.getProperties().put("route_path", routePath); + node.getProperties().put(PROP_ROUTE_PATH, routePath); } if (hasLoaderData) { node.getProperties().put("uses_loader_data", true); diff --git a/src/main/java/io/github/randomcodespace/iq/detector/typescript/SequelizeORMDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/typescript/SequelizeORMDetector.java index 652bd919..129e3e80 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/typescript/SequelizeORMDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/typescript/SequelizeORMDetector.java @@ -1,7 +1,6 @@ package io.github.randomcodespace.iq.detector.typescript; import io.github.randomcodespace.iq.detector.AbstractAntlrDetector; -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.model.CodeEdge; @@ -28,6 +27,9 @@ ) @Component public class SequelizeORMDetector extends AbstractAntlrDetector { + private static final String PROP_FRAMEWORK = "framework"; + private static final String PROP_SEQUELIZE = "sequelize"; + private static final Pattern DEFINE_RE = Pattern.compile( "sequelize\\.define\\s*\\(\\s*['\"](\\w+)['\"]" @@ -92,7 +94,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "sequelize"); + node.getProperties().put(PROP_FRAMEWORK, PROP_SEQUELIZE); nodes.add(node); } @@ -111,7 +113,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "sequelize"); + node.getProperties().put(PROP_FRAMEWORK, PROP_SEQUELIZE); node.getProperties().put("definition", "define"); nodes.add(node); } @@ -132,7 +134,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(filePath); node.setLineStart(line); - node.getProperties().put("framework", "sequelize"); + node.getProperties().put(PROP_FRAMEWORK, PROP_SEQUELIZE); node.getProperties().put("definition", "class"); nodes.add(node); } diff --git a/src/main/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetector.java b/src/main/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetector.java index 5458fade..6514b6ad 100644 --- a/src/main/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetector.java +++ b/src/main/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetector.java @@ -31,6 +31,9 @@ ) @Component public class TypeScriptStructuresDetector extends AbstractAntlrDetector { + private static final String PROP_ASYNC = "async"; + private static final String PROP_TYPESCRIPT = "typescript"; + private static final Pattern INTERFACE_RE = Pattern.compile( "^\\s*(?:export\\s+)?interface\\s+(\\w+)", Pattern.MULTILINE @@ -64,7 +67,7 @@ public String getName() { @Override public Set getSupportedLanguages() { - return Set.of("typescript", "javascript"); + return Set.of(PROP_TYPESCRIPT, "javascript"); } @Override @@ -75,7 +78,7 @@ protected ParseTree parse(DetectorContext ctx) { return null; // triggers regex fallback } if ("typescript".equals(ctx.language())) { - return AntlrParserFactory.parse("typescript", ctx.content()); + return AntlrParserFactory.parse(PROP_TYPESCRIPT, ctx.content()); } // JavaScript files use the JS grammar (or fall back to regex if parse fails) return AntlrParserFactory.parse("javascript", ctx.content()); @@ -163,7 +166,7 @@ public void enterFunctionDeclaration(TypeScriptParser.FunctionDeclarationContext node.setFilePath(fp); node.setLineStart(lineOf(ruleCtx)); if (ruleCtx.Async() != null) { - node.getProperties().put("async", true); + node.getProperties().put(PROP_ASYNC, true); } nodes.add(node); } @@ -324,7 +327,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setFilePath(fp); node.setLineStart(findLineNumber(text, m.start())); if (isDefault) node.getProperties().put("default", true); - if (isAsync) node.getProperties().put("async", true); + if (isAsync) node.getProperties().put(PROP_ASYNC, true); nodes.add(node); existingIds.add(nodeId); } @@ -345,7 +348,7 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) { node.setModule(moduleName); node.setFilePath(fp); node.setLineStart(findLineNumber(text, m.start())); - if (isAsync) node.getProperties().put("async", true); + if (isAsync) node.getProperties().put(PROP_ASYNC, true); nodes.add(node); existingIds.add(nodeId); } diff --git a/src/main/java/io/github/randomcodespace/iq/flow/FlowEngine.java b/src/main/java/io/github/randomcodespace/iq/flow/FlowEngine.java index 6ff1a627..42c0b614 100644 --- a/src/main/java/io/github/randomcodespace/iq/flow/FlowEngine.java +++ b/src/main/java/io/github/randomcodespace/iq/flow/FlowEngine.java @@ -16,15 +16,21 @@ * manually by FlowCommand, FlowController, and BundleCommand.

*/ public class FlowEngine { + private static final String PROP_AUTH = "auth"; + private static final String PROP_CI = "ci"; + private static final String PROP_DEPLOY = "deploy"; + private static final String PROP_OVERVIEW = "overview"; + private static final String PROP_RUNTIME = "runtime"; - public static final List AVAILABLE_VIEWS = List.of("overview", "ci", "deploy", "runtime", "auth"); + + public static final List AVAILABLE_VIEWS = List.of(PROP_OVERVIEW, PROP_CI, PROP_DEPLOY, PROP_RUNTIME, PROP_AUTH); private static final Map> VIEW_BUILDERS = Map.of( - "overview", FlowViews::buildOverview, - "ci", FlowViews::buildCiView, - "deploy", FlowViews::buildDeployView, - "runtime", FlowViews::buildRuntimeView, - "auth", FlowViews::buildAuthView + PROP_OVERVIEW, FlowViews::buildOverview, + PROP_CI, FlowViews::buildCiView, + PROP_DEPLOY, FlowViews::buildDeployView, + PROP_RUNTIME, FlowViews::buildRuntimeView, + PROP_AUTH, FlowViews::buildAuthView ); private final FlowDataSource dataSource; @@ -76,13 +82,13 @@ public String renderInteractive(String projectName) { } public Map getParentContext(String nodeId) { - for (var viewName : List.of("ci", "deploy", "runtime", "auth")) { + for (var viewName : List.of(PROP_CI, PROP_DEPLOY, PROP_RUNTIME, PROP_AUTH)) { var diagram = generate(viewName); for (var sg : diagram.subgraphs()) { for (var node : sg.nodes()) { if (node.id().equals(nodeId)) { var result = new LinkedHashMap(); - result.put("parent_view", "overview"); + result.put("parent_view", PROP_OVERVIEW); result.put("parent_subgraph", sg.id()); result.put("current_view", viewName); return result; diff --git a/src/main/java/io/github/randomcodespace/iq/flow/FlowModels.java b/src/main/java/io/github/randomcodespace/iq/flow/FlowModels.java index 53aebad9..fb170d70 100644 --- a/src/main/java/io/github/randomcodespace/iq/flow/FlowModels.java +++ b/src/main/java/io/github/randomcodespace/iq/flow/FlowModels.java @@ -11,6 +11,8 @@ * Data models for flow diagrams -- the single source of truth for all renderers. */ public final class FlowModels { + private static final String PROP_LABEL = "label"; + private FlowModels() { } @@ -36,7 +38,7 @@ public FlowNode(String id, String label, String kind, Map proper public Map toMap() { var m = new LinkedHashMap(); m.put("id", id); - m.put("label", label); + m.put(PROP_LABEL, label); m.put("kind", kind); m.put("style", style); m.put("properties", properties); @@ -65,7 +67,7 @@ public Map toMap() { var m = new LinkedHashMap(); m.put("source", source); m.put("target", target); - m.put("label", label); + m.put(PROP_LABEL, label); m.put("style", style); return m; } @@ -92,7 +94,7 @@ public FlowSubgraph(String id, String label, List nodes) { public Map toMap() { var m = new LinkedHashMap(); m.put("id", id); - m.put("label", label); + m.put(PROP_LABEL, label); m.put("drill_down_view", drillDownView); m.put("parent_view", parentView); m.put("nodes", nodes.stream().map(FlowNode::toMap).toList()); diff --git a/src/main/java/io/github/randomcodespace/iq/flow/FlowViews.java b/src/main/java/io/github/randomcodespace/iq/flow/FlowViews.java index 3e8a91e0..6d0611e3 100644 --- a/src/main/java/io/github/randomcodespace/iq/flow/FlowViews.java +++ b/src/main/java/io/github/randomcodespace/iq/flow/FlowViews.java @@ -4,7 +4,6 @@ import io.github.randomcodespace.iq.flow.FlowModels.FlowEdge; import io.github.randomcodespace.iq.flow.FlowModels.FlowNode; import io.github.randomcodespace.iq.flow.FlowModels.FlowSubgraph; -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; @@ -23,6 +22,26 @@ * Mirrors the Python implementation's 5 views exactly. */ public final class FlowViews { + private static final String PROP_LR = "LR"; + private static final String PROP_APP_ENDPOINTS = "app_endpoints"; + private static final String PROP_APP_MESSAGING = "app_messaging"; + private static final String PROP_CI = "ci"; + private static final String PROP_CI_JOBS = "ci_jobs"; + private static final String PROP_CI_PIPELINES = "ci_pipelines"; + private static final String PROP_COMPOSE = "compose"; + private static final String PROP_COUNT = "count"; + private static final String PROP_DOCKER = "docker"; + private static final String PROP_ENDPOINT = "endpoint"; + private static final String PROP_ENDPOINTS = "endpoints"; + private static final String PROP_EP_PROTECTED = "ep_protected"; + private static final String PROP_FRONTEND = "frontend"; + private static final String PROP_GUARDS = "guards"; + private static final String PROP_INFRA = "infra"; + private static final String PROP_JOB_ = "job_"; + private static final String PROP_K8S = "k8s"; + private static final String PROP_MIDDLEWARE = "middleware"; + private static final String PROP_TERRAFORM = "terraform"; + private static final String GITLAB_PREFIX = "gitlab:"; @@ -49,14 +68,14 @@ public static FlowDiagram buildOverview(FlowDataSource store) { .filter(n -> isCiNode(n.getId())) .toList(); if (!workflows.isEmpty() || !ciJobs.isEmpty()) { - ciNodes.add(new FlowNode("ci_pipelines", "Pipelines x" + workflows.size(), "pipeline", - Map.of("count", workflows.size()))); + ciNodes.add(new FlowNode(PROP_CI_PIPELINES, "Pipelines x" + workflows.size(), "pipeline", + Map.of(PROP_COUNT, workflows.size()))); if (!ciJobs.isEmpty()) { - ciNodes.add(new FlowNode("ci_jobs", "Jobs x" + ciJobs.size(), "job", - Map.of("count", ciJobs.size()))); - edges.add(new FlowEdge("ci_pipelines", "ci_jobs")); + ciNodes.add(new FlowNode(PROP_CI_JOBS, "Jobs x" + ciJobs.size(), "job", + Map.of(PROP_COUNT, ciJobs.size()))); + edges.add(new FlowEdge(PROP_CI_PIPELINES, PROP_CI_JOBS)); } - subgraphs.add(new FlowSubgraph("ci", "CI/CD Pipeline", ciNodes, "ci")); + subgraphs.add(new FlowSubgraph(PROP_CI, "CI/CD Pipeline", ciNodes, PROP_CI)); } // Infrastructure subgraph @@ -77,23 +96,23 @@ public static FlowDiagram buildOverview(FlowDataSource store) { var infraFlowNodes = new ArrayList(); if (!k8s.isEmpty()) { - infraFlowNodes.add(new FlowNode("infra_k8s", "K8s Resources x" + k8s.size(), "k8s", - Map.of("count", k8s.size()))); + infraFlowNodes.add(new FlowNode("infra_k8s", "K8s Resources x" + k8s.size(), PROP_K8S, + Map.of(PROP_COUNT, k8s.size()))); } if (!docker.isEmpty()) { - infraFlowNodes.add(new FlowNode("infra_docker", "Docker x" + docker.size(), "docker", - Map.of("count", docker.size()))); + infraFlowNodes.add(new FlowNode("infra_docker", "Docker x" + docker.size(), PROP_DOCKER, + Map.of(PROP_COUNT, docker.size()))); } if (!terraform.isEmpty()) { - infraFlowNodes.add(new FlowNode("infra_tf", "Terraform x" + terraform.size(), "terraform", - Map.of("count", terraform.size()))); + infraFlowNodes.add(new FlowNode("infra_tf", "Terraform x" + terraform.size(), PROP_TERRAFORM, + Map.of(PROP_COUNT, terraform.size()))); } if (!otherInfra.isEmpty()) { - infraFlowNodes.add(new FlowNode("infra_other", "Infra x" + otherInfra.size(), "infra", - Map.of("count", otherInfra.size()))); + infraFlowNodes.add(new FlowNode("infra_other", "Infra x" + otherInfra.size(), PROP_INFRA, + Map.of(PROP_COUNT, otherInfra.size()))); } if (!infraFlowNodes.isEmpty()) { - subgraphs.add(new FlowSubgraph("infra", "Infrastructure", infraFlowNodes, "deploy")); + subgraphs.add(new FlowSubgraph(PROP_INFRA, "Infrastructure", infraFlowNodes, "deploy")); } } @@ -111,24 +130,24 @@ public static FlowDiagram buildOverview(FlowDataSource store) { var appNodes = new ArrayList(); if (!endpoints.isEmpty()) { - appNodes.add(new FlowNode("app_endpoints", "Endpoints x" + endpoints.size(), "endpoint", - Map.of("count", endpoints.size()))); + appNodes.add(new FlowNode(PROP_APP_ENDPOINTS, "Endpoints x" + endpoints.size(), PROP_ENDPOINT, + Map.of(PROP_COUNT, endpoints.size()))); } if (!entities.isEmpty()) { appNodes.add(new FlowNode("app_entities", "Entities x" + entities.size(), "entity", - Map.of("count", entities.size()))); + Map.of(PROP_COUNT, entities.size()))); } if (!components.isEmpty()) { appNodes.add(new FlowNode("app_components", "Components x" + components.size(), "component", - Map.of("count", components.size()))); + Map.of(PROP_COUNT, components.size()))); } if (!topics.isEmpty()) { - appNodes.add(new FlowNode("app_messaging", "Topics/Queues x" + topics.size(), "messaging", - Map.of("count", topics.size()))); + appNodes.add(new FlowNode(PROP_APP_MESSAGING, "Topics/Queues x" + topics.size(), "messaging", + Map.of(PROP_COUNT, topics.size()))); } if (!dbConns.isEmpty()) { appNodes.add(new FlowNode("app_database", "DB Connections x" + dbConns.size(), "database", - Map.of("count", dbConns.size()))); + Map.of(PROP_COUNT, dbConns.size()))); } if (appNodes.isEmpty() && (!classes.isEmpty() || !appMethods.isEmpty())) { appNodes.add(new FlowNode("app_code", @@ -138,10 +157,10 @@ public static FlowDiagram buildOverview(FlowDataSource store) { if (!appNodes.isEmpty()) { subgraphs.add(new FlowSubgraph("app", "Application", appNodes, "runtime")); if (!endpoints.isEmpty() && !entities.isEmpty()) { - edges.add(new FlowEdge("app_endpoints", "app_entities", "queries")); + edges.add(new FlowEdge(PROP_APP_ENDPOINTS, "app_entities", "queries")); } if (!endpoints.isEmpty() && appNodes.stream().anyMatch(n -> "app_messaging".equals(n.id()))) { - edges.add(new FlowEdge("app_endpoints", "app_messaging", null, "dotted")); + edges.add(new FlowEdge(PROP_APP_ENDPOINTS, PROP_APP_MESSAGING, null, "dotted")); } } @@ -152,15 +171,15 @@ public static FlowDiagram buildOverview(FlowDataSource store) { var secNodes = new ArrayList(); if (!guards.isEmpty()) { secNodes.add(new FlowNode("sec_guards", "Auth Guards x" + guards.size(), "guard", - Map.of("count", guards.size()))); + Map.of(PROP_COUNT, guards.size()))); } if (!middleware.isEmpty()) { - secNodes.add(new FlowNode("sec_middleware", "Middleware x" + middleware.size(), "middleware", - Map.of("count", middleware.size()))); + secNodes.add(new FlowNode("sec_middleware", "Middleware x" + middleware.size(), PROP_MIDDLEWARE, + Map.of(PROP_COUNT, middleware.size()))); } subgraphs.add(new FlowSubgraph("security", "Security", secNodes, "auth")); if (!guards.isEmpty() && !endpoints.isEmpty()) { - edges.add(new FlowEdge("sec_guards", "app_endpoints", "protects", "thick")); + edges.add(new FlowEdge("sec_guards", PROP_APP_ENDPOINTS, "protects", "thick")); } } @@ -169,7 +188,7 @@ public static FlowDiagram buildOverview(FlowDataSource store) { var infraSg = subgraphs.stream().filter(sg -> "infra".equals(sg.id())).findFirst(); if (infraSg.isPresent() && !infraSg.get().nodes().isEmpty()) { String firstInfra = infraSg.get().nodes().getFirst().id(); - String ciSource = !ciJobs.isEmpty() ? "ci_jobs" : "ci_pipelines"; + String ciSource = !ciJobs.isEmpty() ? PROP_CI_JOBS : PROP_CI_PIPELINES; edges.add(new FlowEdge(ciSource, firstInfra, "deploys")); } } @@ -184,13 +203,13 @@ public static FlowDiagram buildOverview(FlowDataSource store) { var stats = new LinkedHashMap(); stats.put("total_nodes", allNodes.size()); stats.put("total_edges", countEdges(allNodes)); - stats.put("endpoints", endpoints.size()); + stats.put(PROP_ENDPOINTS, endpoints.size()); stats.put("entities", entities.size()); - stats.put("guards", guards.size()); + stats.put(PROP_GUARDS, guards.size()); stats.put("components", components.size()); stats.put("infra_resources", infraNodesRaw.size()); - return new FlowDiagram("Architecture Overview", "overview", "LR", + return new FlowDiagram("Architecture Overview", "overview", PROP_LR, subgraphs, List.of(), edges, stats); } @@ -248,7 +267,7 @@ public static FlowDiagram buildCiView(FlowDataSource store) { props.put(key, j.getProperties().get(key)); } } - jobNodes.add(new FlowNode("job_" + j.getId().replace(":", "_"), j.getLabel(), "job", props)); + jobNodes.add(new FlowNode(PROP_JOB_ + j.getId().replace(":", "_"), j.getLabel(), "job", props)); } subgraphs.add(new FlowSubgraph("wf_" + wf.getId().replace(":", "_"), wf.getLabel(), jobNodes)); } @@ -259,8 +278,8 @@ public static FlowDiagram buildCiView(FlowDataSource store) { for (var edge : node.getEdges()) { if (edge.getKind() == EdgeKind.DEPENDS_ON && isCiNode(edge.getSourceId())) { edges.add(new FlowEdge( - "job_" + edge.getSourceId().replace(":", "_"), - "job_" + edge.getTarget().getId().replace(":", "_"), + PROP_JOB_ + edge.getSourceId().replace(":", "_"), + PROP_JOB_ + edge.getTarget().getId().replace(":", "_"), "needs")); } } @@ -280,7 +299,7 @@ public static FlowDiagram buildCiView(FlowDataSource store) { stats.put("jobs", jobs.size()); stats.put("triggers", triggers.size()); - return new FlowDiagram("CI/CD Pipeline", "ci", "TD", + return new FlowDiagram("CI/CD Pipeline", PROP_CI, "TD", subgraphs, List.of(), edges, stats); } @@ -311,20 +330,20 @@ public static FlowDiagram buildDeployView(FlowDataSource store) { List other = infra.stream().filter(n -> !grouped.contains(n)).toList(); if (!k8s.isEmpty()) { - subgraphs.add(new FlowSubgraph("k8s", "Kubernetes (" + k8s.size() + " resources)", - makeNodes(k8s, "k8s", 20))); + subgraphs.add(new FlowSubgraph(PROP_K8S, "Kubernetes (" + k8s.size() + " resources)", + makeNodes(k8s, PROP_K8S, 20))); } if (!compose.isEmpty()) { - subgraphs.add(new FlowSubgraph("compose", "Docker Compose (" + compose.size() + " services)", - makeNodes(compose, "compose", 20))); + subgraphs.add(new FlowSubgraph(PROP_COMPOSE, "Docker Compose (" + compose.size() + " services)", + makeNodes(compose, PROP_COMPOSE, 20))); } if (!tf.isEmpty()) { - subgraphs.add(new FlowSubgraph("terraform", "Terraform (" + tf.size() + " resources)", + subgraphs.add(new FlowSubgraph(PROP_TERRAFORM, "Terraform (" + tf.size() + " resources)", makeNodes(tf, "tf", 20))); } if (!docker.isEmpty()) { - subgraphs.add(new FlowSubgraph("docker", "Docker (" + docker.size() + " images)", - makeNodes(docker, "docker", 20))); + subgraphs.add(new FlowSubgraph(PROP_DOCKER, "Docker (" + docker.size() + " images)", + makeNodes(docker, PROP_DOCKER, 20))); } if (!other.isEmpty()) { subgraphs.add(new FlowSubgraph("other_infra", "Other (" + other.size() + ")", @@ -350,10 +369,10 @@ public static FlowDiagram buildDeployView(FlowDataSource store) { } var stats = new LinkedHashMap(); - stats.put("k8s", k8s.size()); - stats.put("compose", compose.size()); - stats.put("terraform", tf.size()); - stats.put("docker", docker.size()); + stats.put(PROP_K8S, k8s.size()); + stats.put(PROP_COMPOSE, compose.size()); + stats.put(PROP_TERRAFORM, tf.size()); + stats.put(PROP_DOCKER, docker.size()); return new FlowDiagram("Deployment Topology", "deploy", "TD", subgraphs, List.of(), edges, stats); @@ -386,11 +405,11 @@ public static FlowDiagram buildRuntimeView(FlowDataSource store) { .filter(e -> !"frontend".equals(e.getProperties().get("layer"))) .toList(); if (!feEp.isEmpty()) { - frontendNodes.add(new FlowNode("rt_fe_endpoints", "Frontend Routes x" + feEp.size(), "endpoint")); + frontendNodes.add(new FlowNode("rt_fe_endpoints", "Frontend Routes x" + feEp.size(), PROP_ENDPOINT)); } if (!beEp.isEmpty()) { - backendNodes.add(new FlowNode("rt_be_endpoints", "API Endpoints x" + beEp.size(), "endpoint", - Map.of("count", beEp.size()))); + backendNodes.add(new FlowNode("rt_be_endpoints", "API Endpoints x" + beEp.size(), PROP_ENDPOINT, + Map.of(PROP_COUNT, beEp.size()))); } } @@ -409,7 +428,7 @@ public static FlowDiagram buildRuntimeView(FlowDataSource store) { } if (!frontendNodes.isEmpty()) { - subgraphs.add(new FlowSubgraph("frontend", "Frontend", frontendNodes)); + subgraphs.add(new FlowSubgraph(PROP_FRONTEND, "Frontend", frontendNodes)); } if (!backendNodes.isEmpty()) { subgraphs.add(new FlowSubgraph("backend", "Backend", backendNodes)); @@ -427,13 +446,13 @@ public static FlowDiagram buildRuntimeView(FlowDataSource store) { } var stats = new LinkedHashMap(); - stats.put("endpoints", endpoints.size()); + stats.put(PROP_ENDPOINTS, endpoints.size()); stats.put("entities", entities.size()); stats.put("components", components.size()); stats.put("topics", topics.size()); stats.put("db_connections", dbConns.size()); - return new FlowDiagram("Runtime Architecture", "runtime", "LR", + return new FlowDiagram("Runtime Architecture", "runtime", PROP_LR, subgraphs, List.of(), edges, stats); } @@ -477,34 +496,34 @@ public static FlowDiagram buildAuthView(FlowDataSource store) { for (var entry : guardsByType.entrySet()) { guardNodes.add(new FlowNode("auth_" + entry.getKey(), entry.getKey() + " x" + entry.getValue().size(), "guard", - Map.of("auth_type", entry.getKey(), "count", entry.getValue().size()))); + Map.of("auth_type", entry.getKey(), PROP_COUNT, entry.getValue().size()))); } if (!middleware.isEmpty()) { - guardNodes.add(new FlowNode("auth_middleware", "Middleware x" + middleware.size(), "middleware", - Map.of("count", middleware.size()))); + guardNodes.add(new FlowNode("auth_middleware", "Middleware x" + middleware.size(), PROP_MIDDLEWARE, + Map.of(PROP_COUNT, middleware.size()))); } if (!guardNodes.isEmpty()) { - subgraphs.add(new FlowSubgraph("guards", "Auth Guards", guardNodes)); + subgraphs.add(new FlowSubgraph(PROP_GUARDS, "Auth Guards", guardNodes)); } // Endpoint coverage var epNodes = new ArrayList(); if (!protectedEndpoints.isEmpty()) { - epNodes.add(new FlowNode("ep_protected", "Protected x" + protectedEndpoints.size(), "endpoint", - "success", Map.of("count", protectedEndpoints.size()))); + epNodes.add(new FlowNode(PROP_EP_PROTECTED, "Protected x" + protectedEndpoints.size(), PROP_ENDPOINT, + "success", Map.of(PROP_COUNT, protectedEndpoints.size()))); } if (!unprotectedEndpoints.isEmpty()) { - epNodes.add(new FlowNode("ep_unprotected", "Unprotected x" + unprotectedEndpoints.size(), "endpoint", - "danger", Map.of("count", unprotectedEndpoints.size()))); + epNodes.add(new FlowNode("ep_unprotected", "Unprotected x" + unprotectedEndpoints.size(), PROP_ENDPOINT, + "danger", Map.of(PROP_COUNT, unprotectedEndpoints.size()))); } if (!epNodes.isEmpty()) { - subgraphs.add(new FlowSubgraph("endpoints", "Endpoints", epNodes)); + subgraphs.add(new FlowSubgraph(PROP_ENDPOINTS, "Endpoints", epNodes)); } // Edges: guards -> protected for (var gn : guardNodes) { if (epNodes.stream().anyMatch(n -> "ep_protected".equals(n.id()))) { - edges.add(new FlowEdge(gn.id(), "ep_protected", "protects", "thick")); + edges.add(new FlowEdge(gn.id(), PROP_EP_PROTECTED, "protects", "thick")); } } @@ -512,13 +531,13 @@ public static FlowDiagram buildAuthView(FlowDataSource store) { : (double) protectedEndpoints.size() / endpoints.size() * 100; var stats = new LinkedHashMap(); - stats.put("guards", guards.size()); - stats.put("middleware", middleware.size()); + stats.put(PROP_GUARDS, guards.size()); + stats.put(PROP_MIDDLEWARE, middleware.size()); stats.put("protected", protectedEndpoints.size()); stats.put("unprotected", unprotectedEndpoints.size()); stats.put("coverage_pct", Math.round(coverage * 10.0) / 10.0); - return new FlowDiagram("Auth & Security", "auth", "LR", + return new FlowDiagram("Auth & Security", "auth", PROP_LR, subgraphs, List.of(), edges, stats); } @@ -547,10 +566,10 @@ private static List makeNodes(List nodes, String prefix, int private static String[] resolveGroupIndex(CodeNode node, List k8s, List compose, List tf, List docker, List other) { int idx; - if ((idx = k8s.indexOf(node)) >= 0) return new String[]{"k8s", String.valueOf(idx)}; - if ((idx = compose.indexOf(node)) >= 0) return new String[]{"compose", String.valueOf(idx)}; + if ((idx = k8s.indexOf(node)) >= 0) return new String[]{PROP_K8S, String.valueOf(idx)}; + if ((idx = compose.indexOf(node)) >= 0) return new String[]{PROP_COMPOSE, String.valueOf(idx)}; if ((idx = tf.indexOf(node)) >= 0) return new String[]{"tf", String.valueOf(idx)}; - if ((idx = docker.indexOf(node)) >= 0) return new String[]{"docker", String.valueOf(idx)}; + if ((idx = docker.indexOf(node)) >= 0) return new String[]{PROP_DOCKER, String.valueOf(idx)}; return new String[]{"other", String.valueOf(other.indexOf(node))}; } diff --git a/src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java b/src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java index 6b0a8bb8..1a6602af 100644 --- a/src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java +++ b/src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java @@ -34,6 +34,27 @@ @Service @ConditionalOnBean(GraphRepository.class) public class GraphStore implements FlowDataSource { + private static final String PROP_CNT = "cnt"; + private static final String PROP_CONNECTIONS = "connections"; + private static final String PROP_EXT = "ext"; + private static final String PROP_FILEPATH = "filePath"; + private static final String PROP_ID = "id"; + private static final String PROP_IDS = "ids"; + private static final String PROP_KIND = "kind"; + private static final String PROP_KINDS = "kinds"; + private static final String PROP_LABEL = "label"; + private static final String PROP_LAYER = "layer"; + private static final String PROP_LIMIT = "limit"; + private static final String PROP_METHOD = "method"; + private static final String PROP_MODULE = "module"; + private static final String PROP_NODEID = "nodeId"; + private static final String PROP_OFFSET = "offset"; + private static final String PROP_SOURCE = "source"; + private static final String PROP_SOURCEID = "sourceId"; + private static final String PROP_TARGET = "target"; + private static final String PROP_TARGETID = "targetId"; + private static final String PROP_TEXT = "text"; + private static final Logger log = LoggerFactory.getLogger(GraphStore.class); @@ -79,7 +100,7 @@ public void bulkSave(List nodes) { try (Transaction tx = graphDb.beginTx()) { var result = tx.execute( "MATCH (n) WITH n LIMIT 5000 DETACH DELETE n RETURN count(*) AS cnt"); - deleted = result.hasNext() ? ((Number) result.next().get("cnt")).intValue() : 0; + deleted = result.hasNext() ? ((Number) result.next().get(PROP_CNT)).intValue() : 0; tx.commit(); } } while (deleted > 0); @@ -147,10 +168,10 @@ public void bulkSave(List nodes) { continue; } edgeBatch.add(Map.of( - "sourceId", sourceId, - "targetId", targetId, + PROP_SOURCEID, sourceId, + PROP_TARGETID, targetId, "edgeId", edge.getId(), - "kind", edge.getKind().getValue() + PROP_KIND, edge.getKind().getValue() )); created++; } @@ -178,15 +199,15 @@ public void bulkSave(List nodes) { /** Convert a CodeNode to a flat property map for Cypher SET. */ private Map nodeToProps(CodeNode node) { Map props = new HashMap<>(); - props.put("id", node.getId()); - props.put("kind", node.getKind().getValue()); - props.put("label", node.getLabel()); + props.put(PROP_ID, node.getId()); + props.put(PROP_KIND, node.getKind().getValue()); + props.put(PROP_LABEL, node.getLabel()); if (node.getFqn() != null) props.put("fqn", node.getFqn()); - if (node.getModule() != null) props.put("module", node.getModule()); - if (node.getFilePath() != null) props.put("filePath", node.getFilePath()); + if (node.getModule() != null) props.put(PROP_MODULE, node.getModule()); + if (node.getFilePath() != null) props.put(PROP_FILEPATH, node.getFilePath()); if (node.getLineStart() != null) props.put("lineStart", node.getLineStart()); if (node.getLineEnd() != null) props.put("lineEnd", node.getLineEnd()); - if (node.getLayer() != null) props.put("layer", node.getLayer()); + if (node.getLayer() != null) props.put(PROP_LAYER, node.getLayer()); if (node.getAnnotations() != null && !node.getAnnotations().isEmpty()) { props.put("annotations", String.join(",", node.getAnnotations())); } @@ -208,7 +229,7 @@ private Map nodeToProps(CodeNode node) { public Optional findById(String id) { try (Transaction tx = graphDb.beginTx()) { var result = tx.execute( - "MATCH (n:CodeNode {id: $id}) RETURN n", Map.of("id", id)); + "MATCH (n:CodeNode {id: $id}) RETURN n", Map.of(PROP_ID, id)); if (result.hasNext()) { var neo4jNode = (org.neo4j.graphdb.Node) result.next().get("n"); CodeNode node = nodeFromNeo4j(neo4jNode); @@ -235,36 +256,36 @@ public List findAll() { public List findByKind(NodeKind kind) { return queryNodes("MATCH (n:CodeNode) WHERE n.kind = $kind RETURN n", - Map.of("kind", kind.getValue())); + Map.of(PROP_KIND, kind.getValue())); } public List findByLayer(String layer) { return queryNodes("MATCH (n:CodeNode) WHERE n.layer = $layer RETURN n", - Map.of("layer", layer)); + Map.of(PROP_LAYER, layer)); } public List findByModule(String module) { return queryNodes("MATCH (n:CodeNode) WHERE n.module = $module RETURN n", - Map.of("module", module)); + Map.of(PROP_MODULE, module)); } public List findByFilePath(String filePath) { return queryNodes("MATCH (n:CodeNode) WHERE n.filePath = $filePath RETURN n", - Map.of("filePath", filePath)); + Map.of(PROP_FILEPATH, filePath)); } public List search(String text) { return queryNodes( "CALL db.index.fulltext.queryNodes('search_index', $text) " + "YIELD node RETURN node AS n", - Map.of("text", toLuceneQuery(text))); + Map.of(PROP_TEXT, toLuceneQuery(text))); } public List search(String text, int limit) { return queryNodes( "CALL db.index.fulltext.queryNodes('search_index', $text) " + "YIELD node RETURN node AS n LIMIT $limit", - Map.of("text", toLuceneQuery(text), "limit", limit)); + Map.of(PROP_TEXT, toLuceneQuery(text), PROP_LIMIT, limit)); } /** @@ -276,7 +297,7 @@ public List searchLexical(String text, int limit) { return queryNodes( "CALL db.index.fulltext.queryNodes('lexical_index', $text) " + "YIELD node RETURN node AS n LIMIT $limit", - Map.of("text", toLuceneQuery(text), "limit", limit)); + Map.of(PROP_TEXT, toLuceneQuery(text), PROP_LIMIT, limit)); } /** @@ -308,7 +329,7 @@ private static String toLuceneQuery(String text) { public List findNeighbors(String nodeId) { return queryNodes( "MATCH (n:CodeNode)-[r]-(m:CodeNode) WHERE n.id = $nodeId RETURN m", - Map.of("nodeId", nodeId)); + Map.of(PROP_NODEID, nodeId)); } /** @@ -326,7 +347,7 @@ public Map> findEndpointNeighborsBatch(List nodeI + "MATCH (n:CodeNode {id: nid})-[]-(ep:CodeNode) " + "WHERE ep.kind IN $epKinds " + "RETURN nid AS matchId, ep", - Map.of("ids", nodeIds, "epKinds", epKinds)); + Map.of(PROP_IDS, nodeIds, "epKinds", epKinds)); while (qr.hasNext()) { var row = qr.next(); String matchId = (String) row.get("matchId"); @@ -341,20 +362,20 @@ public Map> findEndpointNeighborsBatch(List nodeI public List findOutgoingNeighbors(String nodeId) { return queryNodes( "MATCH (n:CodeNode)-[r]->(m:CodeNode) WHERE n.id = $nodeId RETURN m", - Map.of("nodeId", nodeId)); + Map.of(PROP_NODEID, nodeId)); } public List findIncomingNeighbors(String nodeId) { return queryNodes( "MATCH (n:CodeNode)<-[r]-(m:CodeNode) WHERE n.id = $nodeId RETURN m", - Map.of("nodeId", nodeId)); + Map.of(PROP_NODEID, nodeId)); } public long count() { try (Transaction tx = graphDb.beginTx()) { var result = tx.execute("MATCH (n:CodeNode) RETURN count(n) AS cnt"); if (result.hasNext()) { - return ((Number) result.next().get("cnt")).longValue(); + return ((Number) result.next().get(PROP_CNT)).longValue(); } return 0; } @@ -367,10 +388,10 @@ public List findShortestPath(String source, String target) { var result = tx.execute( "MATCH p = shortestPath((a:CodeNode {id: $source})-[*..20]-(b:CodeNode {id: $target})) " + "RETURN [n IN nodes(p) | n.id] AS ids", - Map.of("source", source, "target", target)); + Map.of(PROP_SOURCE, source, PROP_TARGET, target)); if (result.hasNext()) { @SuppressWarnings("unchecked") - List ids = (List) result.next().get("ids"); + List ids = (List) result.next().get(PROP_IDS); return ids; } return List.of(); @@ -386,7 +407,7 @@ public List findEgoGraph(String center, int radius) { public List traceImpact(String nodeId, int depth) { return queryNodes( "MATCH (a:CodeNode {id: $nodeId})-[:RELATES_TO*1..$depth]->(b:CodeNode) RETURN DISTINCT b", - Map.of("nodeId", nodeId, "depth", depth)); + Map.of(PROP_NODEID, nodeId, "depth", depth)); } public List> findCycles(int limit) { @@ -395,10 +416,10 @@ public List> findCycles(int limit) { var result = tx.execute( "MATCH p = (a:CodeNode)-[:RELATES_TO*2..10]->(a) " + "RETURN [n IN nodes(p) | n.id] AS ids LIMIT $limit", - Map.of("limit", limit)); + Map.of(PROP_LIMIT, limit)); while (result.hasNext()) { @SuppressWarnings("unchecked") - List ids = (List) result.next().get("ids"); + List ids = (List) result.next().get(PROP_IDS); cycles.add(ids); } } @@ -409,21 +430,21 @@ public List findConsumers(String targetId) { return queryNodes( "MATCH (n:CodeNode)<-[r:RELATES_TO]-(m:CodeNode) " + "WHERE n.id = $targetId AND r.kind IN ['consumes', 'listens'] RETURN m", - Map.of("targetId", targetId)); + Map.of(PROP_TARGETID, targetId)); } public List findProducers(String targetId) { return queryNodes( "MATCH (n:CodeNode)<-[r:RELATES_TO]-(m:CodeNode) " + "WHERE n.id = $targetId AND r.kind IN ['produces', 'publishes'] RETURN m", - Map.of("targetId", targetId)); + Map.of(PROP_TARGETID, targetId)); } public List findCallers(String targetId) { return queryNodes( "MATCH (n:CodeNode)<-[r:RELATES_TO]-(m:CodeNode) " + "WHERE n.id = $targetId AND r.kind = 'calls' RETURN m", - Map.of("targetId", targetId)); + Map.of(PROP_TARGETID, targetId)); } public List findDependencies(String moduleId) { @@ -443,22 +464,22 @@ public List findDependents(String moduleId) { public List findByKindPaginated(String kind, int offset, int limit) { return queryNodes( "MATCH (n:CodeNode) WHERE n.kind = $kind RETURN n SKIP $offset LIMIT $limit", - Map.of("kind", kind, "offset", offset, "limit", limit)); + Map.of(PROP_KIND, kind, PROP_OFFSET, offset, PROP_LIMIT, limit)); } public List findAllPaginated(int offset, int limit) { return queryNodes( "MATCH (n:CodeNode) RETURN n SKIP $offset LIMIT $limit", - Map.of("offset", offset, "limit", limit)); + Map.of(PROP_OFFSET, offset, PROP_LIMIT, limit)); } public long countByKind(String kind) { try (Transaction tx = graphDb.beginTx()) { var result = tx.execute( "MATCH (n:CodeNode) WHERE n.kind = $kind RETURN count(n) AS cnt", - Map.of("kind", kind)); + Map.of(PROP_KIND, kind)); if (result.hasNext()) { - return ((Number) result.next().get("cnt")).longValue(); + return ((Number) result.next().get(PROP_CNT)).longValue(); } return 0; } @@ -470,7 +491,7 @@ public long countEdges() { try (Transaction tx = graphDb.beginTx()) { var result = tx.execute("MATCH ()-[r:RELATES_TO]->() RETURN count(r) AS cnt"); if (result.hasNext()) { - return ((Number) result.next().get("cnt")).longValue(); + return ((Number) result.next().get(PROP_CNT)).longValue(); } return 0; } @@ -482,7 +503,7 @@ public long countDistinctFiles() { "MATCH (n:CodeNode) WHERE n.filePath IS NOT NULL AND n.filePath <> '' " + "RETURN count(DISTINCT n.filePath) AS cnt"); if (result.hasNext()) { - return ((Number) result.next().get("cnt")).longValue(); + return ((Number) result.next().get(PROP_CNT)).longValue(); } return 0; } @@ -502,8 +523,8 @@ public List> countByFileExtension() { while (result.hasNext()) { var row = result.next(); Map m = new LinkedHashMap<>(); - m.put("ext", row.get("ext")); - m.put("cnt", ((Number) row.get("cnt")).longValue()); + m.put(PROP_EXT, row.get(PROP_EXT)); + m.put(PROP_CNT, ((Number) row.get(PROP_CNT)).longValue()); rows.add(m); } } @@ -517,8 +538,8 @@ public List> countNodesByKind() { while (result.hasNext()) { var row = result.next(); Map m = new LinkedHashMap<>(); - m.put("kind", row.get("kind")); - m.put("cnt", ((Number) row.get("cnt")).longValue()); + m.put(PROP_KIND, row.get(PROP_KIND)); + m.put(PROP_CNT, ((Number) row.get(PROP_CNT)).longValue()); rows.add(m); } } @@ -533,8 +554,8 @@ public List> countNodesByLayer() { while (result.hasNext()) { var row = result.next(); Map m = new LinkedHashMap<>(); - m.put("layer", row.get("layer")); - m.put("cnt", ((Number) row.get("cnt")).longValue()); + m.put(PROP_LAYER, row.get(PROP_LAYER)); + m.put(PROP_CNT, ((Number) row.get(PROP_CNT)).longValue()); rows.add(m); } } @@ -548,14 +569,14 @@ public List> findEdgesPaginated(int offset, int limit) { "MATCH (s:CodeNode)-[r:RELATES_TO]->(t:CodeNode) " + "RETURN r.id AS id, r.kind AS kind, s.id AS sourceId, t.id AS targetId " + "SKIP $offset LIMIT $limit", - Map.of("offset", offset, "limit", limit)); + Map.of(PROP_OFFSET, offset, PROP_LIMIT, limit)); while (result.hasNext()) { var row = result.next(); Map m = new LinkedHashMap<>(); - m.put("id", row.get("id")); - m.put("kind", row.get("kind")); - m.put("sourceId", row.get("sourceId")); - m.put("targetId", row.get("targetId")); + m.put(PROP_ID, row.get(PROP_ID)); + m.put(PROP_KIND, row.get(PROP_KIND)); + m.put(PROP_SOURCEID, row.get(PROP_SOURCEID)); + m.put(PROP_TARGETID, row.get(PROP_TARGETID)); rows.add(m); } } @@ -569,14 +590,14 @@ public List> findEdgesByKindPaginated(String kind, int offse "MATCH (s:CodeNode)-[r:RELATES_TO]->(t:CodeNode) WHERE r.kind = $kind " + "RETURN r.id AS id, r.kind AS kind, s.id AS sourceId, t.id AS targetId " + "SKIP $offset LIMIT $limit", - Map.of("kind", kind, "offset", offset, "limit", limit)); + Map.of(PROP_KIND, kind, PROP_OFFSET, offset, PROP_LIMIT, limit)); while (result.hasNext()) { var row = result.next(); Map m = new LinkedHashMap<>(); - m.put("id", row.get("id")); - m.put("kind", row.get("kind")); - m.put("sourceId", row.get("sourceId")); - m.put("targetId", row.get("targetId")); + m.put(PROP_ID, row.get(PROP_ID)); + m.put(PROP_KIND, row.get(PROP_KIND)); + m.put(PROP_SOURCEID, row.get(PROP_SOURCEID)); + m.put(PROP_TARGETID, row.get(PROP_TARGETID)); rows.add(m); } } @@ -599,11 +620,11 @@ public FilePathResult getFilePathsWithCounts(int maxFiles) { + "RETURN n.filePath AS filePath, count(n) AS nodeCount " + "ORDER BY n.filePath " + "LIMIT $limit", - Map.of("limit", (long) (maxFiles + 1))); + Map.of(PROP_LIMIT, (long) (maxFiles + 1))); while (result.hasNext()) { var row = result.next(); Map m = new LinkedHashMap<>(); - m.put("filePath", row.get("filePath")); + m.put(PROP_FILEPATH, row.get(PROP_FILEPATH)); m.put("nodeCount", ((Number) row.get("nodeCount")).longValue()); rows.add(m); } @@ -619,9 +640,9 @@ public long countEdgesByKind(String kind) { try (Transaction tx = graphDb.beginTx()) { var result = tx.execute( "MATCH ()-[r:RELATES_TO]->() WHERE r.kind = $kind RETURN count(r) AS cnt", - Map.of("kind", kind)); + Map.of(PROP_KIND, kind)); if (result.hasNext()) { - return ((Number) result.next().get("cnt")).longValue(); + return ((Number) result.next().get(PROP_CNT)).longValue(); } return 0; } @@ -651,11 +672,11 @@ public List findNodesWithoutIncomingSemantic(List kinds, + "AND NOT n.kind IN $excludeKinds " + "AND NOT EXISTS { MATCH (m)-[r:RELATES_TO]->(n) WHERE r.kind IN $semanticKinds } " + "RETURN n SKIP $offset LIMIT $limit", - Map.of("kinds", kinds, + Map.of(PROP_KINDS, kinds, "semanticKinds", semanticEdgeKinds, "excludeKinds", excludeNodeKinds, - "offset", offset, - "limit", limit)); + PROP_OFFSET, offset, + PROP_LIMIT, limit)); } /** @@ -668,7 +689,7 @@ public List findNodesWithoutIncoming(List kinds, int offset, i "MATCH (n:CodeNode) WHERE n.kind IN $kinds " + "AND NOT EXISTS { MATCH (m)-[:RELATES_TO]->(n) } " + "RETURN n SKIP $offset LIMIT $limit", - Map.of("kinds", kinds, "offset", offset, "limit", limit)); + Map.of(PROP_KINDS, kinds, PROP_OFFSET, offset, PROP_LIMIT, limit)); } // --- Topology queries --- @@ -697,10 +718,10 @@ public Map getTopology() { while (result.hasNext()) { var row = result.next(); Map svc = new LinkedHashMap<>(); - svc.put("id", row.get("id")); - svc.put("label", row.get("label")); - svc.put("kind", row.get("kind")); - svc.put("layer", row.get("layer")); + svc.put(PROP_ID, row.get(PROP_ID)); + svc.put(PROP_LABEL, row.get(PROP_LABEL)); + svc.put(PROP_KIND, row.get(PROP_KIND)); + svc.put(PROP_LAYER, row.get(PROP_LAYER)); Object nc = row.get("node_count"); svc.put("node_count", nc instanceof Number n ? n.longValue() : 0L); services.add(svc); @@ -712,15 +733,15 @@ public Map getTopology() { var result = tx.execute( "MATCH (n:CodeNode) WHERE n.kind IN $kinds " + "RETURN n.id AS id, n.label AS label, n.kind AS kind", - Map.of("kinds", infraKinds)); + Map.of(PROP_KINDS, infraKinds)); while (result.hasNext()) { var row = result.next(); - String id = (String) row.get("id"); - String kind = (String) row.get("kind"); + String id = (String) row.get(PROP_ID); + String kind = (String) row.get(PROP_KIND); Map infra = new LinkedHashMap<>(); - infra.put("id", id); - infra.put("label", row.get("label")); - infra.put("kind", kind); + infra.put(PROP_ID, id); + infra.put(PROP_LABEL, row.get(PROP_LABEL)); + infra.put(PROP_KIND, kind); // Derive type from id prefix (e.g., "postgresql:orders-db" → "postgresql") if (id != null && id.contains(":")) { infra.put("type", id.split(":", 2)[0]); @@ -737,14 +758,14 @@ public Map getTopology() { "MATCH (s:CodeNode)-[r:RELATES_TO]->(t:CodeNode) " + "WHERE s.kind = 'service' AND t.kind IN $kinds " + "RETURN s.id AS source, t.id AS target, r.kind AS kind, count(r) AS cnt", - Map.of("kinds", infraKinds)); + Map.of(PROP_KINDS, infraKinds)); while (result.hasNext()) { var row = result.next(); Map conn = new LinkedHashMap<>(); - conn.put("source", row.get("source")); - conn.put("target", row.get("target")); - conn.put("kind", row.get("kind")); - Object cnt = row.get("cnt"); + conn.put(PROP_SOURCE, row.get(PROP_SOURCE)); + conn.put(PROP_TARGET, row.get(PROP_TARGET)); + conn.put(PROP_KIND, row.get(PROP_KIND)); + Object cnt = row.get(PROP_CNT); conn.put("count", cnt instanceof Number n ? n.longValue() : 0L); connections.add(conn); } @@ -753,7 +774,7 @@ public Map getTopology() { Map topology = new LinkedHashMap<>(); topology.put("services", services); topology.put("infrastructure", infrastructure); - topology.put("connections", connections); + topology.put(PROP_CONNECTIONS, connections); return topology; } @@ -769,7 +790,7 @@ public Map computeAggregateStats() { result.put("languages", computeLanguageStats()); result.put("frameworks", computeFrameworkStats()); result.put("infra", computeInfraStats()); - result.put("connections", computeConnectionStats()); + result.put(PROP_CONNECTIONS, computeConnectionStats()); result.put("auth", computeAuthStats()); result.put("architecture", computeArchitectureStats()); return result; @@ -784,7 +805,7 @@ public Map computeAggregateCategoryStats(String category) { case "languages" -> computeLanguageStats(); case "frameworks" -> computeFrameworkStats(); case "infra" -> computeInfraStats(); - case "connections" -> computeConnectionStats(); + case PROP_CONNECTIONS -> computeConnectionStats(); case "auth" -> computeAuthStats(); case "architecture" -> computeArchitectureStats(); default -> null; @@ -795,13 +816,13 @@ private Map computeGraphStats() { Map graph = new LinkedHashMap<>(); try (Transaction tx = graphDb.beginTx()) { var r1 = tx.execute("MATCH (n:CodeNode) RETURN count(n) AS cnt"); - graph.put("nodes", r1.hasNext() ? ((Number) r1.next().get("cnt")).longValue() : 0L); + graph.put("nodes", r1.hasNext() ? ((Number) r1.next().get(PROP_CNT)).longValue() : 0L); var r2 = tx.execute("MATCH ()-[r:RELATES_TO]->() RETURN count(r) AS cnt"); - graph.put("edges", r2.hasNext() ? ((Number) r2.next().get("cnt")).longValue() : 0L); + graph.put("edges", r2.hasNext() ? ((Number) r2.next().get(PROP_CNT)).longValue() : 0L); var r3 = tx.execute( "MATCH (n:CodeNode) WHERE n.filePath IS NOT NULL AND n.filePath <> '' " + "RETURN count(DISTINCT n.filePath) AS cnt"); - graph.put("files", r3.hasNext() ? ((Number) r3.next().get("cnt")).longValue() : 0L); + graph.put("files", r3.hasNext() ? ((Number) r3.next().get(PROP_CNT)).longValue() : 0L); } return graph; } @@ -816,16 +837,16 @@ private Map computeLanguageStats() { var row = result.next(); String lang = String.valueOf(row.get("lang")).trim(); if (!lang.isBlank()) { - langCounts.merge(lang, ((Number) row.get("cnt")).longValue(), Long::sum); + langCounts.merge(lang, ((Number) row.get(PROP_CNT)).longValue(), Long::sum); } } } if (langCounts.isEmpty()) { for (Map row : countByFileExtension()) { - String ext = String.valueOf(row.get("ext")).trim().toLowerCase(); + String ext = String.valueOf(row.get(PROP_EXT)).trim().toLowerCase(); String lang = extensionToLanguage(ext); if (lang != null) { - langCounts.merge(lang, ((Number) row.get("cnt")).longValue(), Long::sum); + langCounts.merge(lang, ((Number) row.get(PROP_CNT)).longValue(), Long::sum); } } } @@ -842,7 +863,7 @@ private Map computeFrameworkStats() { var row = result.next(); String fw = String.valueOf(row.get("fw")).trim(); if (!fw.isBlank()) { - fwCounts.merge(fw, ((Number) row.get("cnt")).longValue(), Long::sum); + fwCounts.merge(fw, ((Number) row.get(PROP_CNT)).longValue(), Long::sum); } } } @@ -861,7 +882,7 @@ private Map computeInfraStats() { var row = result.next(); String dbType = normalizeDbType(String.valueOf(row.get("dbType"))); if (dbType != null) { - databases.merge(dbType, ((Number) row.get("cnt")).longValue(), Long::sum); + databases.merge(dbType, ((Number) row.get(PROP_CNT)).longValue(), Long::sum); } } } @@ -874,7 +895,7 @@ private Map computeInfraStats() { + "RETURN coalesce(n.prop_protocol, n.label, 'unknown') AS protocol, count(n) AS cnt"); while (result.hasNext()) { var row = result.next(); - messaging.merge(String.valueOf(row.get("protocol")), ((Number) row.get("cnt")).longValue(), Long::sum); + messaging.merge(String.valueOf(row.get("protocol")), ((Number) row.get(PROP_CNT)).longValue(), Long::sum); } } infra.put("messaging", sortByValueDesc(messaging)); @@ -886,7 +907,7 @@ private Map computeInfraStats() { + "RETURN coalesce(n.prop_resource_type, n.label, 'unknown') AS resType, count(n) AS cnt"); while (result.hasNext()) { var row = result.next(); - cloud.merge(String.valueOf(row.get("resType")), ((Number) row.get("cnt")).longValue(), Long::sum); + cloud.merge(String.valueOf(row.get("resType")), ((Number) row.get(PROP_CNT)).longValue(), Long::sum); } } infra.put("cloud", sortByValueDesc(cloud)); @@ -904,7 +925,7 @@ private Map computeConnectionStats() { Map restByMethod = new TreeMap<>(); while (restResult.hasNext()) { var row = restResult.next(); - restByMethod.put(String.valueOf(row.get("method")), ((Number) row.get("cnt")).longValue()); + restByMethod.put(String.valueOf(row.get(PROP_METHOD)), ((Number) row.get(PROP_CNT)).longValue()); } long restTotal = restByMethod.values().stream().mapToLong(Long::longValue).sum(); Map rest = new LinkedHashMap<>(); @@ -914,19 +935,19 @@ private Map computeConnectionStats() { var grpcResult = tx.execute( "MATCH (n:CodeNode) WHERE n.kind = 'endpoint' AND n.prop_protocol = 'grpc' RETURN count(n) AS cnt"); - connections.put("grpc", grpcResult.hasNext() ? ((Number) grpcResult.next().get("cnt")).longValue() : 0L); + connections.put("grpc", grpcResult.hasNext() ? ((Number) grpcResult.next().get(PROP_CNT)).longValue() : 0L); var wsResult = tx.execute( "MATCH (n:CodeNode) WHERE n.kind = 'websocket_endpoint' RETURN count(n) AS cnt"); - connections.put("websocket", wsResult.hasNext() ? ((Number) wsResult.next().get("cnt")).longValue() : 0L); + connections.put("websocket", wsResult.hasNext() ? ((Number) wsResult.next().get(PROP_CNT)).longValue() : 0L); var prodResult = tx.execute( "MATCH ()-[r:RELATES_TO]->() WHERE r.kind IN ['produces', 'publishes'] RETURN count(r) AS cnt"); - connections.put("producers", prodResult.hasNext() ? ((Number) prodResult.next().get("cnt")).longValue() : 0L); + connections.put("producers", prodResult.hasNext() ? ((Number) prodResult.next().get(PROP_CNT)).longValue() : 0L); var consResult = tx.execute( "MATCH ()-[r:RELATES_TO]->() WHERE r.kind IN ['consumes', 'listens'] RETURN count(r) AS cnt"); - connections.put("consumers", consResult.hasNext() ? ((Number) consResult.next().get("cnt")).longValue() : 0L); + connections.put("consumers", consResult.hasNext() ? ((Number) consResult.next().get(PROP_CNT)).longValue() : 0L); } return connections; } @@ -941,7 +962,7 @@ private Map computeAuthStats() { var row = guardResult.next(); String authType = String.valueOf(row.get("authType")).trim(); if (!authType.isBlank()) { - authCounts.merge(authType, ((Number) row.get("cnt")).longValue(), Long::sum); + authCounts.merge(authType, ((Number) row.get(PROP_CNT)).longValue(), Long::sum); } } var fwResult = tx.execute( @@ -952,7 +973,7 @@ private Map computeAuthStats() { String fw = String.valueOf(row.get("fw")).trim(); String authType = fw.substring("auth:".length()).trim(); if (!authType.isEmpty()) { - authCounts.merge(authType, ((Number) row.get("cnt")).longValue(), Long::sum); + authCounts.merge(authType, ((Number) row.get(PROP_CNT)).longValue(), Long::sum); } } } @@ -964,18 +985,18 @@ private Map computeArchitectureStats() { Map kindToLabel = Map.of( "class", "classes", "interface", "interfaces", "abstract_class", "abstract_classes", "enum", "enums", - "annotation_type", "annotation_types", "module", "modules", - "method", "methods"); + "annotation_type", "annotation_types", PROP_MODULE, "modules", + PROP_METHOD, "methods"); List archKinds = List.of("class", "interface", "abstract_class", "enum", - "annotation_type", "module", "method"); + "annotation_type", PROP_MODULE, PROP_METHOD); try (Transaction tx = graphDb.beginTx()) { var result = tx.execute( "MATCH (n:CodeNode) WHERE n.kind IN $kinds RETURN n.kind AS kind, count(n) AS cnt", - Map.of("kinds", archKinds)); + Map.of(PROP_KINDS, archKinds)); while (result.hasNext()) { var row = result.next(); - String kind = (String) row.get("kind"); - long cnt = ((Number) row.get("cnt")).longValue(); + String kind = (String) row.get(PROP_KIND); + long cnt = ((Number) row.get(PROP_CNT)).longValue(); if (cnt > 0) { arch.put(kindToLabel.getOrDefault(kind, kind), cnt); } @@ -1070,10 +1091,10 @@ private void hydrateEdges(List nodes) { + "RETURN r.id AS id, r.kind AS kind, s.id AS sourceId, t.id AS targetId"); while (result.hasNext()) { var row = result.next(); - String sourceId = (String) row.get("sourceId"); - String targetId = (String) row.get("targetId"); - String edgeId = (String) row.get("id"); - String kindStr = (String) row.get("kind"); + String sourceId = (String) row.get(PROP_SOURCEID); + String targetId = (String) row.get(PROP_TARGETID); + String edgeId = (String) row.get(PROP_ID); + String kindStr = (String) row.get(PROP_KIND); CodeNode source = nodeById.get(sourceId); CodeNode target = nodeById.get(targetId); @@ -1098,12 +1119,12 @@ private void hydrateEdgesForNode(Transaction tx, CodeNode node) { var result = tx.execute( "MATCH (s:CodeNode {id: $nodeId})-[r:RELATES_TO]->(t:CodeNode) " + "RETURN r.id AS id, r.kind AS kind, t.id AS targetId, t", - Map.of("nodeId", node.getId())); + Map.of(PROP_NODEID, node.getId())); while (result.hasNext()) { var row = result.next(); - String edgeId = (String) row.get("id"); - String kindStr = (String) row.get("kind"); - String targetId = (String) row.get("targetId"); + String edgeId = (String) row.get(PROP_ID); + String kindStr = (String) row.get(PROP_KIND); + String targetId = (String) row.get(PROP_TARGETID); EdgeKind edgeKind; try { edgeKind = EdgeKind.fromValue(kindStr); @@ -1122,16 +1143,16 @@ private void hydrateEdgesForNode(Transaction tx, CodeNode node) { * This is the key to avoiding OOM — we read only scalar properties. */ private static CodeNode nodeFromNeo4j(org.neo4j.graphdb.Node neo4jNode) { - String id = (String) neo4jNode.getProperty("id", null); - String kindStr = (String) neo4jNode.getProperty("kind", null); + String id = (String) neo4jNode.getProperty(PROP_ID, null); + String kindStr = (String) neo4jNode.getProperty(PROP_KIND, null); NodeKind kind = kindStr != null ? NodeKind.fromValue(kindStr) : NodeKind.MODULE; - String label = (String) neo4jNode.getProperty("label", ""); + String label = (String) neo4jNode.getProperty(PROP_LABEL, ""); CodeNode node = new CodeNode(id, kind, label); node.setFqn((String) neo4jNode.getProperty("fqn", null)); - node.setModule((String) neo4jNode.getProperty("module", null)); - node.setFilePath((String) neo4jNode.getProperty("filePath", null)); - node.setLayer((String) neo4jNode.getProperty("layer", null)); + node.setModule((String) neo4jNode.getProperty(PROP_MODULE, null)); + node.setFilePath((String) neo4jNode.getProperty(PROP_FILEPATH, null)); + node.setLayer((String) neo4jNode.getProperty(PROP_LAYER, null)); Object lineStart = neo4jNode.getProperty("lineStart", null); if (lineStart instanceof Number n) node.setLineStart(n.intValue()); diff --git a/src/main/java/io/github/randomcodespace/iq/intelligence/RepositoryIdentity.java b/src/main/java/io/github/randomcodespace/iq/intelligence/RepositoryIdentity.java index 622e4235..31ce5aa4 100644 --- a/src/main/java/io/github/randomcodespace/iq/intelligence/RepositoryIdentity.java +++ b/src/main/java/io/github/randomcodespace/iq/intelligence/RepositoryIdentity.java @@ -18,15 +18,17 @@ public record RepositoryIdentity( String branch, Instant buildTimestamp ) { + private static final String PROP_HEAD = "HEAD"; + /** * Resolve repository identity from a local path using git commands. * Fields that cannot be determined are set to null gracefully. */ public static RepositoryIdentity resolve(java.nio.file.Path repoPath) { String repoUrl = runGit(repoPath, "remote", "get-url", "origin"); - String commitSha = runGit(repoPath, "rev-parse", "HEAD"); - String branch = runGit(repoPath, "rev-parse", "--abbrev-ref", "HEAD"); - // Detached HEAD produces "HEAD" rather than a branch name — normalise to null + String commitSha = runGit(repoPath, "rev-parse", PROP_HEAD); + String branch = runGit(repoPath, "rev-parse", "--abbrev-ref", PROP_HEAD); + // Detached HEAD produces PROP_HEAD rather than a branch name — normalise to null if ("HEAD".equals(branch)) branch = null; return new RepositoryIdentity(repoUrl, commitSha, branch, Instant.now()); } diff --git a/src/main/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssembler.java b/src/main/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssembler.java index 876837be..c364550b 100644 --- a/src/main/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssembler.java +++ b/src/main/java/io/github/randomcodespace/iq/intelligence/evidence/EvidencePackAssembler.java @@ -34,6 +34,8 @@ @Service @Profile("serving") public class EvidencePackAssembler { + private static final String PROP_UNKNOWN = "unknown"; + private final LexicalQueryService lexicalQueryService; private final SnippetStore snippetStore; @@ -79,7 +81,7 @@ public EvidencePack assemble(EvidencePackRequest request, ArtifactMetadata artif // Determine language from filePath when available (for query planner) String language = request.filePath() != null ? inferLanguage(request.filePath()) - : "unknown"; + : PROP_UNKNOWN; // Plan the query QueryPlan plan = queryPlanner.plan(QueryType.FIND_SYMBOL, language); @@ -126,7 +128,7 @@ public EvidencePack assemble(EvidencePackRequest request, ArtifactMetadata artif if (n.getFilePath() != null) m.put("filePath", n.getFilePath()); if (n.getLineStart() != null) m.put("lineStart", n.getLineStart()); if (n.getLineEnd() != null) m.put("lineEnd", n.getLineEnd()); - m.put("kind", n.getKind() != null ? n.getKind().getValue() : "unknown"); + m.put("kind", n.getKind() != null ? n.getKind().getValue() : PROP_UNKNOWN); if (n.getProperties() != null) { n.getProperties().forEach((k, v) -> { if (k.startsWith("prov_") && v != null) m.put(k, v); @@ -236,9 +238,9 @@ private CapabilityLevel deriveCapabilityLevel(QueryPlan plan) { } private static String inferLanguage(String filePath) { - if (filePath == null) return "unknown"; + if (filePath == null) return PROP_UNKNOWN; int dot = filePath.lastIndexOf('.'); - if (dot < 0) return "unknown"; + if (dot < 0) return PROP_UNKNOWN; return switch (filePath.substring(dot + 1).toLowerCase()) { case "java" -> "java"; case "ts", "tsx" -> "typescript"; @@ -247,7 +249,7 @@ private static String inferLanguage(String filePath) { case "go" -> "go"; case "rs" -> "rust"; case "cs" -> "csharp"; - default -> "unknown"; + default -> PROP_UNKNOWN; }; } } diff --git a/src/main/java/io/github/randomcodespace/iq/intelligence/extractor/LanguageEnricher.java b/src/main/java/io/github/randomcodespace/iq/intelligence/extractor/LanguageEnricher.java index e49419b8..a3cae8d5 100644 --- a/src/main/java/io/github/randomcodespace/iq/intelligence/extractor/LanguageEnricher.java +++ b/src/main/java/io/github/randomcodespace/iq/intelligence/extractor/LanguageEnricher.java @@ -29,15 +29,18 @@ */ @Component public class LanguageEnricher { + private static final String PROP_JAVASCRIPT = "javascript"; + private static final String PROP_TYPESCRIPT = "typescript"; + private static final Logger log = LoggerFactory.getLogger(LanguageEnricher.class); /** * Language alias map: normalises file-extension languages to extractor language keys. - * e.g. "javascript" nodes are handled by the "typescript" extractor. + * e.g. PROP_JAVASCRIPT nodes are handled by the PROP_TYPESCRIPT extractor. */ private static final Map LANGUAGE_ALIASES = Map.of( - "javascript", "typescript" + PROP_JAVASCRIPT, PROP_TYPESCRIPT ); private final List extractors; @@ -150,8 +153,8 @@ static String detectLanguage(String filePath) { if (dot < 0) return null; return switch (filePath.substring(dot + 1).toLowerCase()) { case "java" -> "java"; - case "ts", "tsx" -> "typescript"; - case "js", "jsx", "mjs", "cjs" -> "javascript"; + case "ts", "tsx" -> PROP_TYPESCRIPT; + case "js", "jsx", "mjs", "cjs" -> PROP_JAVASCRIPT; case "py", "pyw" -> "python"; case "go" -> "go"; default -> null; diff --git a/src/main/java/io/github/randomcodespace/iq/intelligence/lexical/SnippetStore.java b/src/main/java/io/github/randomcodespace/iq/intelligence/lexical/SnippetStore.java index 7d147122..4c9aea45 100644 --- a/src/main/java/io/github/randomcodespace/iq/intelligence/lexical/SnippetStore.java +++ b/src/main/java/io/github/randomcodespace/iq/intelligence/lexical/SnippetStore.java @@ -17,6 +17,8 @@ */ @Component public class SnippetStore { + private static final String PROP_UNKNOWN = "unknown"; + /** Maximum lines in any extracted snippet. */ public static final int MAX_LINES = 50; @@ -83,9 +85,9 @@ public Optional extract(CodeNode node, Path rootPath, int contextLi } static String inferLanguage(String filePath) { - if (filePath == null) return "unknown"; + if (filePath == null) return PROP_UNKNOWN; int dot = filePath.lastIndexOf('.'); - if (dot < 0) return "unknown"; + if (dot < 0) return PROP_UNKNOWN; return switch (filePath.substring(dot + 1).toLowerCase()) { case "java" -> "java"; case "ts", "tsx" -> "typescript"; @@ -98,7 +100,7 @@ static String inferLanguage(String filePath) { "h", "hpp" -> "cpp"; case "kt" -> "kotlin"; case "scala", "sc" -> "scala"; - default -> "unknown"; + default -> PROP_UNKNOWN; }; } } diff --git a/src/main/java/io/github/randomcodespace/iq/intelligence/query/CapabilityMatrix.java b/src/main/java/io/github/randomcodespace/iq/intelligence/query/CapabilityMatrix.java index 6779478d..989a30a3 100644 --- a/src/main/java/io/github/randomcodespace/iq/intelligence/query/CapabilityMatrix.java +++ b/src/main/java/io/github/randomcodespace/iq/intelligence/query/CapabilityMatrix.java @@ -27,6 +27,14 @@ *

This class is intentionally non-instantiable. Use the static {@link #get} methods. */ public final class CapabilityMatrix { + private static final String PROP_CPP = "cpp"; + private static final String PROP_CSHARP = "csharp"; + private static final String PROP_GO = "go"; + private static final String PROP_JAVASCRIPT = "javascript"; + private static final String PROP_PYTHON = "python"; + private static final String PROP_RUST = "rust"; + private static final String PROP_TYPESCRIPT = "typescript"; + // ------------------------------------------------------------------ // Language normalisation @@ -34,7 +42,7 @@ public final class CapabilityMatrix { /** Languages with ANTLR or JavaParser AST-level support. */ private static final Set ANTLR_LANGUAGES = - Set.of("typescript", "javascript", "python", "go", "csharp", "rust", "cpp"); + Set.of(PROP_TYPESCRIPT, PROP_JAVASCRIPT, PROP_PYTHON, PROP_GO, PROP_CSHARP, PROP_RUST, PROP_CPP); /** Languages with regex/text-only detection (no grammar). */ private static final Set LEXICAL_ONLY_LANGUAGES = @@ -234,7 +242,7 @@ public static CapabilityLevel get(String language, CapabilityDimension dimension public static Map> asSerializableMap() { Map> result = new TreeMap<>(); for (String lang : new String[]{ - "java", "typescript", "javascript", "python", "go", "csharp", "rust", "cpp", + "java", PROP_TYPESCRIPT, PROP_JAVASCRIPT, PROP_PYTHON, PROP_GO, PROP_CSHARP, PROP_RUST, PROP_CPP, "kotlin", "scala", "ruby", "php", "shell"}) { Map caps = tableFor(lang); Map row = new LinkedHashMap<>(); @@ -258,13 +266,13 @@ private static String normalise(String language) { private static Map tableFor(String lang) { return switch (lang) { case "java" -> JAVA_CAPS; - case "typescript" -> TYPESCRIPT_CAPS; - case "javascript" -> JAVASCRIPT_CAPS; - case "python" -> PYTHON_CAPS; - case "go" -> GO_CAPS; - case "csharp", "c#" -> CSHARP_CAPS; - case "cpp", "c++" -> CPP_CAPS; - case "rust" -> RUST_CAPS; + case PROP_TYPESCRIPT -> TYPESCRIPT_CAPS; + case PROP_JAVASCRIPT -> JAVASCRIPT_CAPS; + case PROP_PYTHON -> PYTHON_CAPS; + case PROP_GO -> GO_CAPS; + case PROP_CSHARP, "c#" -> CSHARP_CAPS; + case PROP_CPP, "c++" -> CPP_CAPS; + case PROP_RUST -> RUST_CAPS; default -> { if (LEXICAL_ONLY_LANGUAGES.contains(lang)) yield LEXICAL_ONLY_CAPS; if (ANTLR_LANGUAGES.contains(lang)) yield CSHARP_CAPS; // cpp etc → PARTIAL diff --git a/src/main/java/io/github/randomcodespace/iq/mcp/McpTools.java b/src/main/java/io/github/randomcodespace/iq/mcp/McpTools.java index ad40fefe..1d2fc22d 100644 --- a/src/main/java/io/github/randomcodespace/iq/mcp/McpTools.java +++ b/src/main/java/io/github/randomcodespace/iq/mcp/McpTools.java @@ -29,7 +29,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; /** * MCP tool definitions using Spring AI annotations. @@ -39,6 +38,8 @@ @Profile("serving") @org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(name = "codeiq.neo4j.enabled", havingValue = "true", matchIfMissing = true) public class McpTools { + private static final String PROP_ERROR = "error"; + private final QueryService queryService; private final CodeIqConfig config; @@ -93,7 +94,7 @@ public String getStats() { try { return toJson(queryService.getStats()); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -103,7 +104,7 @@ public String getDetailedStats( try { return toJson(queryService.getDetailedStats(category != null ? category : "all")); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -114,7 +115,7 @@ public String queryNodes( try { return toJson(queryService.listNodes(kind, limit != null ? limit : 50, 0)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -125,7 +126,7 @@ public String queryEdges( try { return toJson(queryService.listEdges(kind, limit != null ? limit : 50, 0)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -136,7 +137,7 @@ public String getNodeNeighbors( try { return toJson(queryService.getNeighbors(nodeId, direction != null ? direction : "both")); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -147,7 +148,7 @@ public String getEgoGraph( try { return toJson(queryService.egoGraph(center, radius != null ? radius : 2)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -157,7 +158,7 @@ public String findCycles( try { return toJson(queryService.findCycles(limit != null ? limit : 100)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -168,11 +169,11 @@ public String findShortestPath( try { Map result = queryService.shortestPath(source, target); if (result == null) { - return toJson(Map.of("error", "No path found between " + source + " and " + target)); + return toJson(Map.of(PROP_ERROR, "No path found between " + source + " and " + target)); } return toJson(result); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -182,7 +183,7 @@ public String findConsumers( try { return toJson(queryService.consumersOf(targetId)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -192,7 +193,7 @@ public String findProducers( try { return toJson(queryService.producersOf(targetId)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -202,7 +203,7 @@ public String findCallers( try { return toJson(queryService.callersOf(targetId)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -212,7 +213,7 @@ public String findDependencies( try { return toJson(queryService.dependenciesOf(moduleId)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -222,7 +223,7 @@ public String findDependents( try { return toJson(queryService.dependentsOf(moduleId)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -234,7 +235,7 @@ public String findDeadCode( int safeLimit = limit != null ? Math.min(limit, 1000) : 100; return toJson(queryService.findDeadCode(kind, safeLimit)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -247,14 +248,14 @@ public String generateFlow( try { FlowEngine engine = resolveFlowEngine(); if (engine == null) { - return toJson(Map.of("error", "No analysis data available. Run 'code-iq analyze' first.")); + return toJson(Map.of(PROP_ERROR, "No analysis data available. Run 'code-iq analyze' first.")); } FlowDiagram diagram = engine.generate(viewName); String rendered = engine.render(diagram, fmt); return rendered; } catch (IllegalArgumentException e) { Map error = new LinkedHashMap<>(); - error.put("error", e.getMessage()); + error.put(PROP_ERROR, e.getMessage()); return toJson(error); } } @@ -275,7 +276,7 @@ public String runCypher( for (String pattern : BLOCKED_PATTERNS) { if (java.util.regex.Pattern.compile(pattern).matcher(upper).find()) { String keyword = pattern.replace("\\b", "").replace("\\s+", " "); - return toJson(Map.of("error", "Read-only queries only. Mutation keyword found: " + keyword)); + return toJson(Map.of(PROP_ERROR, "Read-only queries only. Mutation keyword found: " + keyword)); } } try { @@ -299,7 +300,7 @@ public String runCypher( response.put("count", rows.size()); return toJson(response); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -311,7 +312,7 @@ public String findComponentByFile( try { return toJson(queryService.findComponentByFile(filePath)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -322,7 +323,7 @@ public String traceImpact( try { return toJson(queryService.traceImpact(nodeId, depth != null ? depth : 3)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -332,7 +333,7 @@ public String findRelatedEndpoints( try { return toJson(queryService.findRelatedEndpoints(identifier)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -343,7 +344,7 @@ public String searchGraph( try { return toJson(queryService.searchGraph(query, limit != null ? limit : 20)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -363,7 +364,7 @@ public String getCapabilities( } return toJson(result); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -377,7 +378,7 @@ public String readFile( Path resolved = root.resolve(filePath).normalize(); // Path traversal protection if (!resolved.startsWith(root)) { - return toJson(Map.of("error", "Path traversal detected")); + return toJson(Map.of(PROP_ERROR, "Path traversal detected")); } String content = java.nio.file.Files.readString(resolved, java.nio.charset.StandardCharsets.UTF_8); if (startLine != null || endLine != null) { @@ -396,7 +397,7 @@ public String readFile( } return content; } catch (Exception e) { - return toJson(Map.of("error", "Failed to read file: " + e.getMessage())); + return toJson(Map.of(PROP_ERROR, "Failed to read file: " + e.getMessage())); } } @@ -407,7 +408,7 @@ public String getTopology() { try { return toJson(queryService.getTopology()); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -418,7 +419,7 @@ public String serviceDetail( var data = getCachedData(); return toJson(topologyService.serviceDetail(serviceName, data.nodes(), data.edges())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -429,7 +430,7 @@ public String serviceDependencies( var data = getCachedData(); return toJson(topologyService.serviceDependencies(serviceName, data.nodes(), data.edges())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -440,7 +441,7 @@ public String serviceDependents( var data = getCachedData(); return toJson(topologyService.serviceDependents(serviceName, data.nodes(), data.edges())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -451,7 +452,7 @@ public String blastRadius( var data = getCachedData(); return toJson(topologyService.blastRadius(nodeId, data.nodes(), data.edges())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -463,7 +464,7 @@ public String findPath( var data = getCachedData(); return toJson(topologyService.findPath(source, target, data.nodes(), data.edges())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -473,7 +474,7 @@ public String findBottlenecks() { var data = getCachedData(); return toJson(topologyService.findBottlenecks(data.nodes(), data.edges())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -483,7 +484,7 @@ public String findCircularDeps() { var data = getCachedData(); return toJson(topologyService.findCircularDeps(data.nodes(), data.edges())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -493,7 +494,7 @@ public String findDeadServices() { var data = getCachedData(); return toJson(topologyService.findDeadServices(data.nodes(), data.edges())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -504,7 +505,7 @@ public String findNode( var data = getCachedData(); return toJson(topologyService.findNode(query, data.nodes())); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @@ -515,7 +516,7 @@ public String getEvidencePack( @McpToolParam(description = "Max lines per snippet (default: config value)", required = false) Integer maxSnippetLines, @McpToolParam(description = "Include cross-reference nodes (default: false)", required = false) Boolean includeReferences) { if (evidencePackAssembler == null) { - return toJson(Map.of("error", "Evidence pack service unavailable. Run 'enrich' first.")); + return toJson(Map.of(PROP_ERROR, "Evidence pack service unavailable. Run 'enrich' first.")); } try { EvidencePackRequest request = new EvidencePackRequest( @@ -523,19 +524,19 @@ public String getEvidencePack( Boolean.TRUE.equals(includeReferences)); return toJson(evidencePackAssembler.assemble(request, artifactMetadata)); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } @McpTool(name = "get_artifact_metadata", description = "Return artifact metadata: repo identity, commit SHA, build timestamp, extractor versions, capability matrix snapshot, and integrity hash.") public String getArtifactMetadata() { if (artifactMetadata == null) { - return toJson(Map.of("error", "Artifact metadata unavailable. Run 'enrich' first.")); + return toJson(Map.of(PROP_ERROR, "Artifact metadata unavailable. Run 'enrich' first.")); } try { return toJson(artifactMetadata); } catch (Exception e) { - return toJson(Map.of("error", e.getMessage())); + return toJson(Map.of(PROP_ERROR, e.getMessage())); } } diff --git a/src/main/java/io/github/randomcodespace/iq/query/QueryService.java b/src/main/java/io/github/randomcodespace/iq/query/QueryService.java index 154f4326..a8a9b11d 100644 --- a/src/main/java/io/github/randomcodespace/iq/query/QueryService.java +++ b/src/main/java/io/github/randomcodespace/iq/query/QueryService.java @@ -24,6 +24,22 @@ @Service @ConditionalOnBean(GraphStore.class) public class QueryService { + private static final String PROP_CHILDREN = "children"; + private static final String PROP_CNT = "cnt"; + private static final String PROP_COUNT = "count"; + private static final String PROP_DIRECTORY = "directory"; + private static final String PROP_FILE = "file"; + private static final String PROP_ID = "id"; + private static final String PROP_KIND = "kind"; + private static final String PROP_LAYER = "layer"; + private static final String PROP_MODULE = "module"; + private static final String PROP_NODECOUNT = "nodeCount"; + private static final String PROP_NODES = "nodes"; + private static final String PROP_PATH = "path"; + private static final String PROP_SOURCE = "source"; + private static final String PROP_TARGET = "target"; + private static final String PROP_TOTAL = "total"; + private final GraphStore graphStore; private final CodeIqConfig config; @@ -41,18 +57,18 @@ public Map getStats() { // Also include raw counts and breakdowns for backward compat Map nodesByKind = new LinkedHashMap<>(); for (Map row : graphStore.countNodesByKind()) { - nodesByKind.put((String) row.get("kind"), ((Number) row.get("cnt")).longValue()); + nodesByKind.put((String) row.get(PROP_KIND), ((Number) row.get(PROP_CNT)).longValue()); } Map nodesByLayer = new LinkedHashMap<>(); for (Map row : graphStore.countNodesByLayer()) { - nodesByLayer.put((String) row.get("layer"), ((Number) row.get("cnt")).longValue()); + nodesByLayer.put((String) row.get(PROP_LAYER), ((Number) row.get(PROP_CNT)).longValue()); } // Read from already-computed graph sub-map instead of re-querying @SuppressWarnings("unchecked") Map graphStats = (Map) result.get("graph"); if (graphStats != null) { - result.put("node_count", graphStats.get("nodes")); + result.put("node_count", graphStats.get(PROP_NODES)); result.put("edge_count", graphStats.get("edges")); } else { result.put("node_count", graphStore.count()); @@ -92,21 +108,21 @@ public Map listKinds() { List> kinds = new ArrayList<>(); rawCounts.stream() .sorted((a, b) -> Long.compare( - ((Number) b.get("cnt")).longValue(), - ((Number) a.get("cnt")).longValue())) + ((Number) b.get(PROP_CNT)).longValue(), + ((Number) a.get(PROP_CNT)).longValue())) .forEach(row -> { Map m = new LinkedHashMap<>(); - m.put("kind", row.get("kind")); - m.put("count", ((Number) row.get("cnt")).longValue()); + m.put(PROP_KIND, row.get(PROP_KIND)); + m.put(PROP_COUNT, ((Number) row.get(PROP_CNT)).longValue()); kinds.add(m); }); long totalNodes = rawCounts.stream() - .mapToLong(r -> ((Number) r.get("cnt")).longValue()) + .mapToLong(r -> ((Number) r.get(PROP_CNT)).longValue()) .sum(); Map result = new LinkedHashMap<>(); result.put("kinds", kinds); - result.put("total", totalNodes); + result.put(PROP_TOTAL, totalNodes); return result; } @@ -116,11 +132,11 @@ public Map nodesByKind(String kind, int limit, int offset) { long total = graphStore.countByKind(kind); Map result = new LinkedHashMap<>(); - result.put("kind", kind); - result.put("total", total); + result.put(PROP_KIND, kind); + result.put(PROP_TOTAL, total); result.put("offset", offset); result.put("limit", limit); - result.put("nodes", nodes.stream().map(this::nodeToMap).toList()); + result.put(PROP_NODES, nodes.stream().map(this::nodeToMap).toList()); return result; } @@ -133,8 +149,8 @@ public Map listNodes(String kind, int limit, int offset) { } Map result = new LinkedHashMap<>(); - result.put("nodes", nodes.stream().map(this::nodeToMap).toList()); - result.put("count", nodes.size()); + result.put(PROP_NODES, nodes.stream().map(this::nodeToMap).toList()); + result.put(PROP_COUNT, nodes.size()); result.put("offset", offset); result.put("limit", limit); return result; @@ -153,17 +169,17 @@ public Map listEdges(String kind, int limit, int offset) { List> edges = rawEdges.stream().map(row -> { Map m = new LinkedHashMap<>(); - m.put("id", row.get("id")); - m.put("kind", row.get("kind")); - m.put("source", row.get("sourceId")); - m.put("target", row.get("targetId")); + m.put(PROP_ID, row.get(PROP_ID)); + m.put(PROP_KIND, row.get(PROP_KIND)); + m.put(PROP_SOURCE, row.get("sourceId")); + m.put(PROP_TARGET, row.get("targetId")); return m; }).toList(); Map result = new LinkedHashMap<>(); result.put("edges", edges); - result.put("count", edges.size()); - result.put("total", total); + result.put(PROP_COUNT, edges.size()); + result.put(PROP_TOTAL, total); return result; } @@ -196,7 +212,7 @@ public Map getNeighbors(String nodeId, String direction) { result.put("node_id", nodeId); result.put("direction", direction); result.put("neighbors", neighbors.stream().map(this::nodeToMap).toList()); - result.put("count", neighbors.size()); + result.put(PROP_COUNT, neighbors.size()); return result; } @@ -208,9 +224,9 @@ public Map shortestPath(String source, String target) { return null; } Map result = new LinkedHashMap<>(); - result.put("source", source); - result.put("target", target); - result.put("path", path); + result.put(PROP_SOURCE, source); + result.put(PROP_TARGET, target); + result.put(PROP_PATH, path); result.put("length", path.size() - 1); return result; } @@ -220,7 +236,7 @@ public Map findCycles(int limit) { List> cycles = graphStore.findCycles(cappedLimit); Map result = new LinkedHashMap<>(); result.put("cycles", cycles); - result.put("count", cycles.size()); + result.put(PROP_COUNT, cycles.size()); return result; } @@ -230,10 +246,10 @@ public Map traceImpact(String nodeId, int depth) { List impacted = graphStore.traceImpact(nodeId, cappedDepth); Map result = new LinkedHashMap<>(); - result.put("source", nodeId); + result.put(PROP_SOURCE, nodeId); result.put("depth", cappedDepth); result.put("impacted", impacted.stream().map(this::nodeToMap).toList()); - result.put("count", impacted.size()); + result.put(PROP_COUNT, impacted.size()); return result; } @@ -251,8 +267,8 @@ public Map egoGraph(String center, int radius) { Map result = new LinkedHashMap<>(); result.put("center", center); result.put("radius", cappedRadius); - result.put("nodes", nodes.stream().map(this::nodeToMap).toList()); - result.put("count", nodes.size()); + result.put(PROP_NODES, nodes.stream().map(this::nodeToMap).toList()); + result.put(PROP_COUNT, nodes.size()); return result; } @@ -262,18 +278,18 @@ public Map egoGraph(String center, int radius) { public Map consumersOf(String targetId) { List consumers = graphStore.findConsumers(targetId); Map result = new LinkedHashMap<>(); - result.put("target", targetId); + result.put(PROP_TARGET, targetId); result.put("consumers", consumers.stream().map(this::nodeToMap).toList()); - result.put("count", consumers.size()); + result.put(PROP_COUNT, consumers.size()); return result; } public Map producersOf(String targetId) { List producers = graphStore.findProducers(targetId); Map result = new LinkedHashMap<>(); - result.put("target", targetId); + result.put(PROP_TARGET, targetId); result.put("producers", producers.stream().map(this::nodeToMap).toList()); - result.put("count", producers.size()); + result.put(PROP_COUNT, producers.size()); return result; } @@ -281,27 +297,27 @@ public Map producersOf(String targetId) { public Map callersOf(String targetId) { List callers = graphStore.findCallers(targetId); Map result = new LinkedHashMap<>(); - result.put("target", targetId); + result.put(PROP_TARGET, targetId); result.put("callers", callers.stream().map(this::nodeToMap).toList()); - result.put("count", callers.size()); + result.put(PROP_COUNT, callers.size()); return result; } public Map dependenciesOf(String moduleId) { List deps = graphStore.findDependencies(moduleId); Map result = new LinkedHashMap<>(); - result.put("module", moduleId); + result.put(PROP_MODULE, moduleId); result.put("dependencies", deps.stream().map(this::nodeToMap).toList()); - result.put("count", deps.size()); + result.put(PROP_COUNT, deps.size()); return result; } public Map dependentsOf(String moduleId) { List deps = graphStore.findDependents(moduleId); Map result = new LinkedHashMap<>(); - result.put("module", moduleId); + result.put(PROP_MODULE, moduleId); result.put("dependents", deps.stream().map(this::nodeToMap).toList()); - result.put("count", deps.size()); + result.put(PROP_COUNT, deps.size()); return result; } @@ -311,13 +327,13 @@ public Map dependentsOf(String moduleId) { public Map findComponentByFile(String filePath) { List nodes = graphStore.findByFilePath(filePath); Map result = new LinkedHashMap<>(); - result.put("file", filePath); - result.put("nodes", nodes.stream().map(this::nodeToMap).toList()); - result.put("count", nodes.size()); + result.put(PROP_FILE, filePath); + result.put(PROP_NODES, nodes.stream().map(this::nodeToMap).toList()); + result.put(PROP_COUNT, nodes.size()); if (!nodes.isEmpty()) { CodeNode first = nodes.getFirst(); - result.put("module", first.getModule()); + result.put(PROP_MODULE, first.getModule()); result.put("layer", first.getLayer()); } return result; @@ -343,17 +359,17 @@ public Map getFileTree(Integer maxDepth) { GraphStore.FilePathResult filePathResult = graphStore.getFilePathsWithCounts(config.getMaxFiles()); List> rows = filePathResult.rows(); - TreeNode root = new TreeNode("", "directory"); + TreeNode root = new TreeNode("", PROP_DIRECTORY); for (Map row : rows) { String filePath = (String) row.get("filePath"); - long count = ((Number) row.get("nodeCount")).longValue(); + long count = ((Number) row.get(PROP_NODECOUNT)).longValue(); String[] parts = filePath.split("/", -1); TreeNode current = root; for (int i = 0; i < parts.length; i++) { String part = parts[i]; if (part.isEmpty()) continue; boolean isFile = (i == parts.length - 1); - String type = isFile ? "file" : "directory"; + String type = isFile ? PROP_FILE : PROP_DIRECTORY; TreeNode child = current.children.computeIfAbsent(part, k -> new TreeNode(k, type)); if (isFile) { child.nodeCount += count; @@ -386,13 +402,13 @@ private List> buildTreeOutput(TreeNode node, Integer maxDept String childPath = parentPath.isEmpty() ? child.name : parentPath + "/" + child.name; Map m = new LinkedHashMap<>(); m.put("name", child.name); - m.put("path", childPath); - m.put("type", "directory"); - m.put("nodeCount", aggregateCount(child)); + m.put(PROP_PATH, childPath); + m.put("type", PROP_DIRECTORY); + m.put(PROP_NODECOUNT, aggregateCount(child)); if (maxDepth == null || currentDepth < maxDepth) { - m.put("children", buildTreeOutput(child, maxDepth, currentDepth + 1, childPath)); + m.put(PROP_CHILDREN, buildTreeOutput(child, maxDepth, currentDepth + 1, childPath)); } else { - m.put("children", List.of()); + m.put(PROP_CHILDREN, List.of()); } output.add(m); } @@ -400,10 +416,10 @@ private List> buildTreeOutput(TreeNode node, Integer maxDept String childPath = parentPath.isEmpty() ? child.name : parentPath + "/" + child.name; Map m = new LinkedHashMap<>(); m.put("name", child.name); - m.put("path", childPath); - m.put("type", "file"); - m.put("nodeCount", child.nodeCount); - m.put("children", List.of()); + m.put(PROP_PATH, childPath); + m.put("type", PROP_FILE); + m.put(PROP_NODECOUNT, child.nodeCount); + m.put(PROP_CHILDREN, List.of()); output.add(m); } return output; @@ -468,7 +484,7 @@ public Map findRelatedEndpoints(String identifier) { Map result = new LinkedHashMap<>(); result.put("identifier", identifier); result.put("endpoints", endpoints); - result.put("count", endpoints.size()); + result.put(PROP_COUNT, endpoints.size()); result.put("searched_nodes", matches.size()); return result; } @@ -533,17 +549,17 @@ public Map findDeadCode(String kind, int limit) { List> deadCode = deadNodes.stream() .map(n -> { Map m = new LinkedHashMap<>(); - m.put("id", n.getId()); - m.put("kind", n.getKind().getValue()); + m.put(PROP_ID, n.getId()); + m.put(PROP_KIND, n.getKind().getValue()); m.put("label", n.getLabel()); - m.put("file", n.getFilePath()); + m.put(PROP_FILE, n.getFilePath()); return m; }) .toList(); Map result = new LinkedHashMap<>(); result.put("dead_code", deadCode); - result.put("count", deadCode.size()); + result.put(PROP_COUNT, deadCode.size()); return result; } @@ -551,15 +567,15 @@ public Map findDeadCode(String kind, int limit) { Map nodeToMap(CodeNode node) { Map m = new LinkedHashMap<>(); - m.put("id", node.getId()); - m.put("kind", node.getKind().getValue()); + m.put(PROP_ID, node.getId()); + m.put(PROP_KIND, node.getKind().getValue()); m.put("label", node.getLabel()); if (node.getFqn() != null) m.put("fqn", node.getFqn()); - if (node.getModule() != null) m.put("module", node.getModule()); + if (node.getModule() != null) m.put(PROP_MODULE, node.getModule()); if (node.getFilePath() != null) m.put("file_path", node.getFilePath()); if (node.getLineStart() != null) m.put("line_start", node.getLineStart()); if (node.getLineEnd() != null) m.put("line_end", node.getLineEnd()); - if (node.getLayer() != null) m.put("layer", node.getLayer()); + if (node.getLayer() != null) m.put(PROP_LAYER, node.getLayer()); if (node.getAnnotations() != null && !node.getAnnotations().isEmpty()) { m.put("annotations", node.getAnnotations()); } @@ -571,11 +587,11 @@ Map nodeToMap(CodeNode node) { private Map edgeToMap(CodeEdge edge) { Map m = new LinkedHashMap<>(); - m.put("id", edge.getId()); - m.put("kind", edge.getKind().getValue()); - m.put("source", edge.getSourceId()); + m.put(PROP_ID, edge.getId()); + m.put(PROP_KIND, edge.getKind().getValue()); + m.put(PROP_SOURCE, edge.getSourceId()); if (edge.getTarget() != null) { - m.put("target", edge.getTarget().getId()); + m.put(PROP_TARGET, edge.getTarget().getId()); m.put("target_label", edge.getTarget().getLabel()); m.put("target_kind", edge.getTarget().getKind().getValue()); } diff --git a/src/main/java/io/github/randomcodespace/iq/query/TopologyService.java b/src/main/java/io/github/randomcodespace/iq/query/TopologyService.java index b784c2d0..10ffca74 100644 --- a/src/main/java/io/github/randomcodespace/iq/query/TopologyService.java +++ b/src/main/java/io/github/randomcodespace/iq/query/TopologyService.java @@ -29,6 +29,15 @@ */ @Service public class TopologyService { + private static final String PROP_CONNECTIONS = "connections"; + private static final String PROP_ENDPOINT_COUNT = "endpoint_count"; + private static final String PROP_ENTITY_COUNT = "entity_count"; + private static final String PROP_SERVICE = "service"; + private static final String PROP_SOURCE = "source"; + private static final String PROP_TARGET = "target"; + private static final String PROP_TOTAL_CONNECTIONS = "total_connections"; + private static final String PROP_TYPE = "type"; + /** Edge kinds that represent runtime service-to-service connections. */ private static final Set RUNTIME_EDGES = EnumSet.of( @@ -59,15 +68,15 @@ public Map getTopology(List nodes, List edge Map svcMap = new LinkedHashMap<>(); svcMap.put("name", svc.getLabel()); svcMap.put("build_tool", svc.getProperties().getOrDefault("build_tool", "unknown")); - svcMap.put("endpoint_count", svc.getProperties().getOrDefault("endpoint_count", 0)); - svcMap.put("entity_count", svc.getProperties().getOrDefault("entity_count", 0)); + svcMap.put(PROP_ENDPOINT_COUNT, svc.getProperties().getOrDefault(PROP_ENDPOINT_COUNT, 0)); + svcMap.put(PROP_ENTITY_COUNT, svc.getProperties().getOrDefault(PROP_ENTITY_COUNT, 0)); String name = svc.getLabel(); long outCount = connections.stream() - .filter(c -> name.equals(c.get("source"))) + .filter(c -> name.equals(c.get(PROP_SOURCE))) .count(); long inCount = connections.stream() - .filter(c -> name.equals(c.get("target"))) + .filter(c -> name.equals(c.get(PROP_TARGET))) .count(); svcMap.put("connections_out", outCount); svcMap.put("connections_in", inCount); @@ -76,7 +85,7 @@ public Map getTopology(List nodes, List edge Map result = new LinkedHashMap<>(); result.put("services", services); - result.put("connections", connections); + result.put(PROP_CONNECTIONS, connections); result.put("service_count", services.size()); result.put("connection_count", connections.size()); return result; @@ -119,8 +128,8 @@ public Map serviceDetail(String serviceName, List node // Connections involving this service Map nodeToService = buildNodeToServiceMap(nodes); List> connections = findCrossServiceConnections(edges, nodeToService); - result.put("connections", connections.stream() - .filter(c -> serviceName.equals(c.get("source")) || serviceName.equals(c.get("target"))) + result.put(PROP_CONNECTIONS, connections.stream() + .filter(c -> serviceName.equals(c.get(PROP_SOURCE)) || serviceName.equals(c.get(PROP_TARGET))) .toList()); result.put("files", childNodes.stream() @@ -141,17 +150,17 @@ public Map serviceDependencies(String serviceName, List> connections = findCrossServiceConnections(edges, nodeToService); List> deps = connections.stream() - .filter(c -> serviceName.equals(c.get("source"))) + .filter(c -> serviceName.equals(c.get(PROP_SOURCE))) .toList(); Set depServices = deps.stream() - .map(c -> (String) c.get("target")) + .map(c -> (String) c.get(PROP_TARGET)) .collect(Collectors.toCollection(LinkedHashSet::new)); Map result = new LinkedHashMap<>(); - result.put("service", serviceName); + result.put(PROP_SERVICE, serviceName); result.put("depends_on", depServices); - result.put("connections", deps); + result.put(PROP_CONNECTIONS, deps); result.put("count", depServices.size()); return result; } @@ -164,17 +173,17 @@ public Map serviceDependents(String serviceName, List List> connections = findCrossServiceConnections(edges, nodeToService); List> deps = connections.stream() - .filter(c -> serviceName.equals(c.get("target"))) + .filter(c -> serviceName.equals(c.get(PROP_TARGET))) .toList(); Set dependentServices = deps.stream() - .map(c -> (String) c.get("source")) + .map(c -> (String) c.get(PROP_SOURCE)) .collect(Collectors.toCollection(LinkedHashSet::new)); Map result = new LinkedHashMap<>(); - result.put("service", serviceName); + result.put(PROP_SERVICE, serviceName); result.put("depended_by", dependentServices); - result.put("connections", deps); + result.put(PROP_CONNECTIONS, deps); result.put("count", dependentServices.size()); return result; } @@ -238,7 +247,7 @@ public Map blastRadius(String nodeId, List nodes, List } Map result = new LinkedHashMap<>(); - result.put("source", nodeId); + result.put(PROP_SOURCE, nodeId); result.put("affected_services", affectedServices); result.put("affected_nodes", affectedNodes); result.put("affected_service_count", affectedServices.size()); @@ -257,8 +266,8 @@ public List> findPath(String source, String target, // Build service-level adjacency Map>> adj = new HashMap<>(); for (Map conn : connections) { - String src = (String) conn.get("source"); - String tgt = (String) conn.get("target"); + String src = (String) conn.get(PROP_SOURCE); + String tgt = (String) conn.get(PROP_TARGET); adj.computeIfAbsent(src, k -> new HashMap<>()).putIfAbsent(tgt, conn); } @@ -280,7 +289,7 @@ public List> findPath(String source, String target, Map step = new LinkedHashMap<>(); step.put("from", path.get(i)); step.put("to", path.get(i + 1)); - step.put("type", hop.getOrDefault("type", "unknown")); + step.put(PROP_TYPE, hop.getOrDefault(PROP_TYPE, "unknown")); result.add(step); } return result; @@ -309,8 +318,8 @@ public List> findBottlenecks(List nodes, List outDegree = new HashMap<>(); for (Map conn : connections) { - String src = (String) conn.get("source"); - String tgt = (String) conn.get("target"); + String src = (String) conn.get(PROP_SOURCE); + String tgt = (String) conn.get(PROP_TARGET); outDegree.merge(src, 1, Integer::sum); inDegree.merge(tgt, 1, Integer::sum); } @@ -325,18 +334,18 @@ public List> findBottlenecks(List nodes, List 0) { Map entry = new LinkedHashMap<>(); - entry.put("service", name); + entry.put(PROP_SERVICE, name); entry.put("connections_in", in); entry.put("connections_out", out); - entry.put("total_connections", total); + entry.put(PROP_TOTAL_CONNECTIONS, total); result.add(entry); } } // Sort by total connections descending result.sort((a, b) -> Integer.compare( - (int) b.get("total_connections"), - (int) a.get("total_connections"))); + (int) b.get(PROP_TOTAL_CONNECTIONS), + (int) a.get(PROP_TOTAL_CONNECTIONS))); return result; } @@ -350,8 +359,8 @@ public List> findCircularDeps(List nodes, List // Build adjacency Map> adj = new HashMap<>(); for (Map conn : connections) { - String src = (String) conn.get("source"); - String tgt = (String) conn.get("target"); + String src = (String) conn.get(PROP_SOURCE); + String tgt = (String) conn.get(PROP_TARGET); adj.computeIfAbsent(src, k -> new LinkedHashSet<>()).add(tgt); } @@ -359,7 +368,7 @@ public List> findCircularDeps(List nodes, List List> cycles = new ArrayList<>(); Set allServices = new LinkedHashSet<>(adj.keySet()); for (Map conn : connections) { - allServices.add((String) conn.get("target")); + allServices.add((String) conn.get(PROP_TARGET)); } Set globalVisited = new HashSet<>(); @@ -423,16 +432,16 @@ public List> findDeadServices(List nodes, List serviceNodes = findServiceNodes(nodes); Set hasIncoming = connections.stream() - .map(c -> (String) c.get("target")) + .map(c -> (String) c.get(PROP_TARGET)) .collect(Collectors.toSet()); List> result = new ArrayList<>(); for (CodeNode svc : sortedValues(serviceNodes)) { if (!hasIncoming.contains(svc.getLabel())) { Map entry = new LinkedHashMap<>(); - entry.put("service", svc.getLabel()); - entry.put("endpoint_count", svc.getProperties().getOrDefault("endpoint_count", 0)); - entry.put("entity_count", svc.getProperties().getOrDefault("entity_count", 0)); + entry.put(PROP_SERVICE, svc.getLabel()); + entry.put(PROP_ENDPOINT_COUNT, svc.getProperties().getOrDefault(PROP_ENDPOINT_COUNT, 0)); + entry.put(PROP_ENTITY_COUNT, svc.getProperties().getOrDefault(PROP_ENTITY_COUNT, 0)); result.add(entry); } } @@ -485,7 +494,7 @@ private Map findServiceNodes(List nodes) { private Map buildNodeToServiceMap(List nodes) { Map map = new HashMap<>(); for (CodeNode node : nodes) { - Object svc = node.getProperties().get("service"); + Object svc = node.getProperties().get(PROP_SERVICE); if (svc instanceof String s && !s.isBlank()) { map.put(node.getId(), s); } @@ -517,9 +526,9 @@ private List> findCrossServiceConnections( String key = sourceSvc + "->" + targetSvc + ":" + edge.getKind().getValue(); if (seen.add(key)) { Map conn = new LinkedHashMap<>(); - conn.put("source", sourceSvc); - conn.put("target", targetSvc); - conn.put("type", edge.getKind().getValue()); + conn.put(PROP_SOURCE, sourceSvc); + conn.put(PROP_TARGET, targetSvc); + conn.put(PROP_TYPE, edge.getKind().getValue()); connections.add(conn); } } @@ -534,8 +543,8 @@ private Map nodeToCompact(CodeNode node) { m.put("label", node.getLabel()); if (node.getFilePath() != null) m.put("file_path", node.getFilePath()); if (node.getLayer() != null) m.put("layer", node.getLayer()); - Object svc = node.getProperties().get("service"); - if (svc != null) m.put("service", svc); + Object svc = node.getProperties().get(PROP_SERVICE); + if (svc != null) m.put(PROP_SERVICE, svc); return m; } @@ -553,7 +562,7 @@ private static List sorted(Set set) { } private static String serviceOf(CodeNode node) { - Object svc = node.getProperties().get("service"); + Object svc = node.getProperties().get(PROP_SERVICE); return svc instanceof String s ? s : null; } } diff --git a/src/test/java/io/github/randomcodespace/iq/analyzer/StructuredParserTest.java b/src/test/java/io/github/randomcodespace/iq/analyzer/StructuredParserTest.java index 4a12a687..795c6b2e 100644 --- a/src/test/java/io/github/randomcodespace/iq/analyzer/StructuredParserTest.java +++ b/src/test/java/io/github/randomcodespace/iq/analyzer/StructuredParserTest.java @@ -69,7 +69,7 @@ void parsesNestedYaml() { @Test void invalidYamlReturnsNull() { // SnakeYAML is quite lenient, but truly broken input should not crash - Object result = parser.parse("yaml", ":::\n---\n{{invalid", "bad.yaml"); + parser.parse("yaml", ":::\n---\n{{invalid", "bad.yaml"); // May return null or a partial parse — just don't throw // (SnakeYAML treats many things as strings, so this might not be null) }