diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..369fab44 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,4 @@ +{ + "prefersReducedMotion": true, + "spinnerTipsEnabled": false +} 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 3903e3fa..0e73f9e5 100644 --- a/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java +++ b/src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java @@ -522,6 +522,7 @@ private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int b var batchExecutorService = parallelism != null && parallelism > 0 ? Executors.newFixedThreadPool(parallelism) : Executors.newVirtualThreadPerTaskExecutor(); + try (batchExecutorService) { List batch = new ArrayList<>(batchSize); for (int fileIdx = 0; fileIdx < files.size(); fileIdx++) { batch.add(files.get(fileIdx)); @@ -636,7 +637,7 @@ private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int b batch.clear(); } } - batchExecutorService.close(); + } // close batchExecutorService if (cacheHits > 0) { report.accept("Cache hits: " + cacheHits + " / " + totalFiles + " files"); @@ -1290,15 +1291,6 @@ private static List compileExcludePatterns(List .toList(); } - /** - * Check whether a file path matches any of the given pre-compiled exclude patterns. - */ - private static boolean matchesAnyExclude(String filePath, List excludePatterns) { - if (excludePatterns == null) return false; - List compiled = compileExcludePatterns(excludePatterns); - return matchesAnyCompiledExclude(filePath, compiled); - } - /** * Check whether a file path matches any of the given pre-compiled patterns. */ diff --git a/src/main/java/io/github/randomcodespace/iq/api/GraphController.java b/src/main/java/io/github/randomcodespace/iq/api/GraphController.java index 57b3ebdc..60210ead 100644 --- a/src/main/java/io/github/randomcodespace/iq/api/GraphController.java +++ b/src/main/java/io/github/randomcodespace/iq/api/GraphController.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -196,13 +197,6 @@ public List> searchGraph( return queryService.searchGraph(q, Math.min(limit, 1000)); } - /** - * Check whether Neo4j (via QueryService) is available for queries. - */ - private boolean useNeo4j() { - return queryService != null; - } - private void requireQueryService() { if (queryService == null) { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, diff --git a/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java b/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java index 89347020..1e783dfd 100644 --- a/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java +++ b/src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java @@ -30,8 +30,6 @@ public class ServeCommand implements Callable { private static final Logger log = LoggerFactory.getLogger(ServeCommand.class); - public static final String COMMAND_NAME = "serve"; - @Parameters(index = "0", defaultValue = ".", description = "Path to analyzed codebase") private Path path; diff --git a/src/main/java/io/github/randomcodespace/iq/config/ProjectConfigLoader.java b/src/main/java/io/github/randomcodespace/iq/config/ProjectConfigLoader.java index 26a36750..bb9bf376 100644 --- a/src/main/java/io/github/randomcodespace/iq/config/ProjectConfigLoader.java +++ b/src/main/java/io/github/randomcodespace/iq/config/ProjectConfigLoader.java @@ -162,21 +162,8 @@ private static void applyOverrides(Map data, CodeIqConfig config if (data.containsKey("max_radius")) { config.setMaxRadius(toInt(data.get("max_radius"), config.getMaxRadius())); } - // Nested analysis section (matches Python config structure) - if (data.get("analysis") instanceof Map analysis) { - if (analysis.containsKey("parallelism")) { - // Stored for CLI to pick up; not directly in CodeIqConfig - } - if (analysis.containsKey("incremental")) { - // Available for future use - } - } - // Nested output section - if (data.get("output") instanceof Map output) { - if (output.containsKey("max_nodes")) { - // Available for future use - } - } + // Nested analysis/output sections are recognized but not yet mapped to CodeIqConfig. + // They are loaded and accessible via data map for CLI commands that need them. } private static int toInt(Object value, int defaultValue) { 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 51e720c4..bee550c4 100644 --- a/src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java +++ b/src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java @@ -21,6 +21,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; /** * Facade service over the Neo4j graph backend. @@ -400,7 +402,7 @@ public long countEdges() { public long countDistinctFiles() { try (Transaction tx = graphDb.beginTx()) { var result = tx.execute( - "MATCH (n:CodeNode) WHERE n.filePath IS NOT NULL " + "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(); @@ -647,6 +649,260 @@ public Map getTopology() { return topology; } + // --- Stats aggregation queries (Cypher-only, no node hydration) --- + + /** + * Compute all categorized stats via Cypher aggregation. + * Never loads full nodes into heap — safe for 100K+ node graphs. + */ + public Map computeAggregateStats() { + Map result = new LinkedHashMap<>(); + result.put("graph", computeGraphStats()); + result.put("languages", computeLanguageStats()); + result.put("frameworks", computeFrameworkStats()); + result.put("infra", computeInfraStats()); + result.put("connections", computeConnectionStats()); + result.put("auth", computeAuthStats()); + result.put("architecture", computeArchitectureStats()); + return result; + } + + /** + * Compute stats for a single category via Cypher aggregation. + */ + public Map computeAggregateCategoryStats(String category) { + return switch (category.toLowerCase()) { + case "graph" -> computeGraphStats(); + case "languages" -> computeLanguageStats(); + case "frameworks" -> computeFrameworkStats(); + case "infra" -> computeInfraStats(); + case "connections" -> computeConnectionStats(); + case "auth" -> computeAuthStats(); + case "architecture" -> computeArchitectureStats(); + default -> null; + }; + } + + private Map computeGraphStats() { + Map graph = new LinkedHashMap<>(); + graph.put("nodes", count()); + graph.put("edges", countEdges()); + graph.put("files", countDistinctFiles()); + return graph; + } + + private Map computeLanguageStats() { + Map langCounts = new TreeMap<>(); + try (Transaction tx = graphDb.beginTx()) { + var result = tx.execute( + "MATCH (n:CodeNode) WHERE n.prop_language IS NOT NULL " + + "RETURN toLower(n.prop_language) AS lang, count(n) AS cnt"); + while (result.hasNext()) { + 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); + } + } + } + if (langCounts.isEmpty()) { + for (Map row : countByFileExtension()) { + String ext = String.valueOf(row.get("ext")).trim().toLowerCase(); + String lang = extensionToLanguage(ext); + if (lang != null) { + langCounts.merge(lang, ((Number) row.get("cnt")).longValue(), Long::sum); + } + } + } + return new LinkedHashMap<>(sortByValueDesc(langCounts)); + } + + private Map computeFrameworkStats() { + Map fwCounts = new TreeMap<>(); + try (Transaction tx = graphDb.beginTx()) { + var result = tx.execute( + "MATCH (n:CodeNode) WHERE n.prop_framework IS NOT NULL " + + "RETURN n.prop_framework AS fw, count(n) AS cnt"); + while (result.hasNext()) { + 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); + } + } + } + return new LinkedHashMap<>(sortByValueDesc(fwCounts)); + } + + private Map computeInfraStats() { + Map infra = new LinkedHashMap<>(); + + Map databases = new TreeMap<>(); + try (Transaction tx = graphDb.beginTx()) { + var result = tx.execute( + "MATCH (n:CodeNode) WHERE n.kind = 'database_connection' AND n.prop_db_type IS NOT NULL " + + "RETURN n.prop_db_type AS dbType, count(n) AS cnt"); + while (result.hasNext()) { + 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); + } + } + } + infra.put("databases", sortByValueDesc(databases)); + + Map messaging = new TreeMap<>(); + try (Transaction tx = graphDb.beginTx()) { + var result = tx.execute( + "MATCH (n:CodeNode) WHERE n.kind IN ['topic', 'queue', 'message_queue'] " + + "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); + } + } + infra.put("messaging", sortByValueDesc(messaging)); + + Map cloud = new TreeMap<>(); + try (Transaction tx = graphDb.beginTx()) { + var result = tx.execute( + "MATCH (n:CodeNode) WHERE n.kind IN ['azure_resource', 'infra_resource'] " + + "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); + } + } + infra.put("cloud", sortByValueDesc(cloud)); + + return infra; + } + + private Map computeConnectionStats() { + Map connections = new LinkedHashMap<>(); + try (Transaction tx = graphDb.beginTx()) { + var restResult = tx.execute( + "MATCH (n:CodeNode) WHERE n.kind = 'endpoint' " + + "AND (n.prop_protocol IS NULL OR n.prop_protocol <> 'grpc') " + + "RETURN coalesce(toUpper(n.prop_http_method), 'UNKNOWN') AS method, count(n) AS cnt"); + Map restByMethod = new TreeMap<>(); + while (restResult.hasNext()) { + var row = restResult.next(); + restByMethod.put(String.valueOf(row.get("method")), ((Number) row.get("cnt")).longValue()); + } + long restTotal = restByMethod.values().stream().mapToLong(Long::longValue).sum(); + Map rest = new LinkedHashMap<>(); + rest.put("total", restTotal); + rest.put("by_method", sortByValueDesc(restByMethod)); + connections.put("rest", rest); + + 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); + + 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); + + 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); + + 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); + } + return connections; + } + + private Map computeAuthStats() { + Map authCounts = new TreeMap<>(); + try (Transaction tx = graphDb.beginTx()) { + var guardResult = tx.execute( + "MATCH (n:CodeNode) WHERE n.kind = 'guard' AND n.prop_auth_type IS NOT NULL " + + "RETURN n.prop_auth_type AS authType, count(n) AS cnt"); + while (guardResult.hasNext()) { + 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); + } + } + var fwResult = tx.execute( + "MATCH (n:CodeNode) WHERE n.prop_framework STARTS WITH 'auth:' " + + "RETURN n.prop_framework AS fw, count(n) AS cnt"); + while (fwResult.hasNext()) { + var row = fwResult.next(); + 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); + } + } + } + return new LinkedHashMap<>(sortByValueDesc(authCounts)); + } + + private Map computeArchitectureStats() { + Map arch = new LinkedHashMap<>(); + Map kindToLabel = Map.of( + "class", "classes", "interface", "interfaces", + "abstract_class", "abstract_classes", "enum", "enums", + "annotation_type", "annotation_types", "module", "modules", + "method", "methods"); + List archKinds = List.of("class", "interface", "abstract_class", "enum", + "annotation_type", "module", "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)); + while (result.hasNext()) { + var row = result.next(); + String kind = (String) row.get("kind"); + long cnt = ((Number) row.get("cnt")).longValue(); + if (cnt > 0) { + arch.put(kindToLabel.getOrDefault(kind, kind), cnt); + } + } + } + return arch; + } + + private static final Map STATS_DB_TYPE_NORMALIZE = Map.ofEntries( + Map.entry("mysql", "MySQL"), Map.entry("postgresql", "PostgreSQL"), + Map.entry("postgres", "PostgreSQL"), Map.entry("sqlserver", "SQL Server"), + Map.entry("mssql", "SQL Server"), Map.entry("oracle", "Oracle"), + Map.entry("h2", "H2"), Map.entry("sqlite", "SQLite"), + Map.entry("mariadb", "MariaDB"), Map.entry("mongo", "MongoDB"), + Map.entry("mongodb", "MongoDB"), Map.entry("redis", "Redis"), + Map.entry("neo4j", "Neo4j")); + + private static String normalizeDbType(String raw) { + String lower = raw.trim().toLowerCase(); + if (lower.contains("@")) lower = lower.substring(0, lower.indexOf('@')); + return STATS_DB_TYPE_NORMALIZE.getOrDefault(lower, raw.trim()); + } + + private static String extensionToLanguage(String ext) { + return switch (ext) { + case "java" -> "java"; case "kt", "kts" -> "kotlin"; + case "py" -> "python"; case "js", "mjs", "cjs" -> "javascript"; + case "ts", "tsx" -> "typescript"; case "go" -> "go"; + case "rs" -> "rust"; case "cs" -> "csharp"; + case "scala" -> "scala"; case "cpp", "cc", "cxx" -> "cpp"; + case "proto" -> "protobuf"; default -> null; + }; + } + + private static Map sortByValueDesc(Map map) { + return map.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .collect(Collectors.toMap( + Map.Entry::getKey, Map.Entry::getValue, + (a, b) -> a, LinkedHashMap::new)); + } + // --- Internal helpers --- /** 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 aab558cf..26555192 100644 --- a/src/main/java/io/github/randomcodespace/iq/query/QueryService.java +++ b/src/main/java/io/github/randomcodespace/iq/query/QueryService.java @@ -25,23 +25,16 @@ public class QueryService { private final GraphStore graphStore; private final CodeIqConfig config; - private final StatsService statsService; - public QueryService(GraphStore graphStore, CodeIqConfig config, StatsService statsService) { + public QueryService(GraphStore graphStore, CodeIqConfig config) { this.graphStore = graphStore; this.config = config; - this.statsService = statsService; } @Cacheable("graph-stats") public Map getStats() { - // Load full graph data and compute rich categorized stats - List nodes = graphStore.findAll(); - List edges = nodes.stream() - .flatMap(n -> n.getEdges().stream()) - .toList(); - - Map result = statsService.computeStats(nodes, edges); + // Use Cypher aggregation — never loads full nodes into heap + Map result = graphStore.computeAggregateStats(); // Also include raw counts and breakdowns for backward compat Map nodesByKind = new LinkedHashMap<>(); @@ -53,8 +46,16 @@ public Map getStats() { nodesByLayer.put((String) row.get("layer"), ((Number) row.get("cnt")).longValue()); } - result.put("node_count", nodes.size()); - result.put("edge_count", edges.size()); + // 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("edge_count", graphStats.get("edges")); + } else { + result.put("node_count", graphStore.count()); + result.put("edge_count", graphStore.countEdges()); + } result.put("nodes_by_kind", nodesByKind); result.put("nodes_by_layer", nodesByLayer); return result; @@ -66,15 +67,11 @@ public Map getStats() { */ @Cacheable(value = "detailed-stats", key = "#category") public Map getDetailedStats(String category) { - List nodes = graphStore.findAll(); - List edges = nodes.stream() - .flatMap(n -> n.getEdges().stream()) - .toList(); - + // Use Cypher aggregation — never loads full nodes into heap if (category == null || "all".equalsIgnoreCase(category)) { - return statsService.computeStats(nodes, edges); + return graphStore.computeAggregateStats(); } - Map catResult = statsService.computeCategory(nodes, edges, category); + Map catResult = graphStore.computeAggregateCategoryStats(category); if (catResult == null) { Map error = new LinkedHashMap<>(); error.put("error", "Unknown category: " + category diff --git a/src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java b/src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java index 03462599..14f51a97 100644 --- a/src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java +++ b/src/test/java/io/github/randomcodespace/iq/api/TopologyEndpointTest.java @@ -184,7 +184,7 @@ void getTopologyReturnsEmptyListsWhenNoData() throws Exception { @Test void queryServiceGetTopologyDelegatesToGraphStore() { CodeIqConfig cfg = new CodeIqConfig(); - QueryService service = new QueryService(graphStore, cfg, new io.github.randomcodespace.iq.query.StatsService()); + QueryService service = new QueryService(graphStore, cfg); when(graphStore.getTopology()).thenReturn(buildTopologyResponse()); Map result = service.getTopology(); @@ -199,7 +199,7 @@ void queryServiceGetTopologyDelegatesToGraphStore() { @Test void queryServiceGetTopologyReturnsServicesInfraConnections() { CodeIqConfig cfg = new CodeIqConfig(); - QueryService service = new QueryService(graphStore, cfg, new io.github.randomcodespace.iq.query.StatsService()); + QueryService service = new QueryService(graphStore, cfg); Map topology = buildTopologyResponse(); when(graphStore.getTopology()).thenReturn(topology); diff --git a/src/test/java/io/github/randomcodespace/iq/cli/ServeCommandTest.java b/src/test/java/io/github/randomcodespace/iq/cli/ServeCommandTest.java index 5fcdfdc2..fc730f7f 100644 --- a/src/test/java/io/github/randomcodespace/iq/cli/ServeCommandTest.java +++ b/src/test/java/io/github/randomcodespace/iq/cli/ServeCommandTest.java @@ -15,11 +15,6 @@ void commandNameIsServe() { assertEquals("serve", cmdLine.getCommandName()); } - @Test - void commandNameConstantMatchesAnnotation() { - assertEquals("serve", ServeCommand.COMMAND_NAME); - } - @Test void defaultPortIs8080() { var cmd = new ServeCommand(); diff --git a/src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java b/src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java index 0c8f9464..da4750a6 100644 --- a/src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java +++ b/src/test/java/io/github/randomcodespace/iq/query/QueryServiceTest.java @@ -36,8 +36,7 @@ void setUp() { config = new CodeIqConfig(); config.setMaxDepth(10); config.setMaxRadius(10); - statsService = new StatsService(); - service = new QueryService(graphStore, config, statsService); + service = new QueryService(graphStore, config); } private CodeNode makeNode(String id, NodeKind kind, String label) { @@ -61,14 +60,26 @@ private CodeNode makeNodeWithEdge(String id, NodeKind kind, String label, @Test void getStatsShouldReturnNodeAndEdgeCounts() { - var endpoint = makeNodeWithEdge("ep:1", NodeKind.ENDPOINT, "GET /users", - "svc:1", EdgeKind.CALLS); - endpoint.getProperties().put("http_method", "GET"); - endpoint.setFilePath("src/Main.java"); - var cls = makeNode("cls:1", NodeKind.CLASS, "UserService"); - cls.setFilePath("src/UserService.java"); - cls.setEdges(new ArrayList<>()); - when(graphStore.findAll()).thenReturn(List.of(endpoint, cls)); + // Mock Cypher aggregation from GraphStore + Map aggregateStats = new java.util.LinkedHashMap<>(); + aggregateStats.put("graph", Map.of("nodes", 2L, "edges", 1L, "files", 2L)); + aggregateStats.put("languages", Map.of("java", 2L)); + aggregateStats.put("frameworks", Map.of()); + aggregateStats.put("infra", Map.of("databases", Map.of(), "messaging", Map.of(), "cloud", Map.of())); + Map rest = new java.util.LinkedHashMap<>(); + rest.put("total", 1L); + rest.put("by_method", Map.of("GET", 1L)); + Map connections = new java.util.LinkedHashMap<>(); + connections.put("rest", rest); + connections.put("grpc", 0L); + connections.put("websocket", 0L); + connections.put("producers", 0L); + connections.put("consumers", 0L); + aggregateStats.put("connections", connections); + aggregateStats.put("auth", Map.of()); + aggregateStats.put("architecture", Map.of("classes", 1L)); + + when(graphStore.computeAggregateStats()).thenReturn(aggregateStats); when(graphStore.countNodesByKind()).thenReturn(List.of( Map.of("kind", "endpoint", "cnt", 1L), Map.of("kind", "class", "cnt", 1L))); @@ -77,11 +88,10 @@ void getStatsShouldReturnNodeAndEdgeCounts() { Map stats = service.getStats(); - // ComputedStatsResponse format — graph section from StatsService @SuppressWarnings("unchecked") Map graph = (Map) stats.get("graph"); - assertEquals(2, graph.get("nodes")); - assertEquals(1, graph.get("edges")); + assertEquals(2L, graph.get("nodes")); + assertEquals(1L, graph.get("edges")); assertEquals(2L, graph.get("files")); assertNotNull(stats.get("languages")); assertNotNull(stats.get("frameworks")); @@ -89,15 +99,13 @@ void getStatsShouldReturnNodeAndEdgeCounts() { assertNotNull(stats.get("connections")); assertNotNull(stats.get("auth")); assertNotNull(stats.get("architecture")); - // REST endpoint detection @SuppressWarnings("unchecked") - Map connections = (Map) stats.get("connections"); + Map resultConnections = (Map) stats.get("connections"); @SuppressWarnings("unchecked") - Map rest = (Map) connections.get("rest"); - assertEquals(1L, rest.get("total")); - // Backward compat - assertEquals(2, stats.get("node_count")); - assertEquals(1, stats.get("edge_count")); + Map resultRest = (Map) resultConnections.get("rest"); + assertEquals(1L, resultRest.get("total")); + assertEquals(2L, stats.get("node_count")); + assertEquals(1L, stats.get("edge_count")); assertNotNull(stats.get("nodes_by_kind")); assertNotNull(stats.get("nodes_by_layer")); }