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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"prefersReducedMotion": true,
"spinnerTipsEnabled": false
}
12 changes: 2 additions & 10 deletions src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<DiscoveredFile> batch = new ArrayList<>(batchSize);
for (int fileIdx = 0; fileIdx < files.size(); fileIdx++) {
batch.add(files.get(fileIdx));
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -1290,15 +1291,6 @@ private static List<java.util.regex.Pattern> compileExcludePatterns(List<String>
.toList();
}

/**
* Check whether a file path matches any of the given pre-compiled exclude patterns.
*/
private static boolean matchesAnyExclude(String filePath, List<String> excludePatterns) {
if (excludePatterns == null) return false;
List<java.util.regex.Pattern> compiled = compileExcludePatterns(excludePatterns);
return matchesAnyCompiledExclude(filePath, compiled);
}

/**
* Check whether a file path matches any of the given pre-compiled patterns.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Check warning on line 11 in src/main/java/io/github/randomcodespace/iq/api/GraphController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'org.springframework.web.bind.annotation.PostMapping'.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1FfJdQgLsdgtcoYp4d&open=AZ1FfJdQgLsdgtcoYp4d&pullRequest=3
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -196,13 +197,6 @@
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ public class ServeCommand implements Callable<Integer> {

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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,21 +162,8 @@ private static void applyOverrides(Map<String, Object> 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) {
Expand Down
258 changes: 257 additions & 1 deletion src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -66,7 +68,7 @@
* Creates an index on CodeNode.id for fast MATCH during edge creation.
* Logs progress every 10K items for visibility on large graphs.
*/
public void bulkSave(List<CodeNode> nodes) {

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

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

Expand Down Expand Up @@ -400,7 +402,7 @@
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();
Expand Down Expand Up @@ -643,10 +645,264 @@
Map<String, Object> topology = new LinkedHashMap<>();
topology.put("services", services);
topology.put("infrastructure", infrastructure);
topology.put("connections", connections);

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "connections" 3 times.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1FUB8T9DoLcXMm7keM&open=AZ1FUB8T9DoLcXMm7keM&pullRequest=3
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<String, Object> computeAggregateStats() {
Map<String, Object> 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<String, Object> 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<String, Object> computeGraphStats() {
Map<String, Object> graph = new LinkedHashMap<>();
graph.put("nodes", count());
graph.put("edges", countEdges());
graph.put("files", countDistinctFiles());
return graph;
}

private Map<String, Object> computeLanguageStats() {
Map<String, Long> 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<String, Object> 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<String, Object> computeFrameworkStats() {
Map<String, Long> 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<String, Object> computeInfraStats() {
Map<String, Object> infra = new LinkedHashMap<>();

Map<String, Long> 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<String, Long> 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<String, Long> 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<String, Object> computeConnectionStats() {
Map<String, Object> 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<String, Long> restByMethod = new TreeMap<>();
while (restResult.hasNext()) {
var row = restResult.next();
restByMethod.put(String.valueOf(row.get("method")), ((Number) row.get("cnt")).longValue());

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "method" 3 times.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1FUB8T9DoLcXMm7keL&open=AZ1FUB8T9DoLcXMm7keL&pullRequest=3
}
long restTotal = restByMethod.values().stream().mapToLong(Long::longValue).sum();
Map<String, Object> 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<String, Object> computeAuthStats() {
Map<String, Long> 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<String, Object> computeArchitectureStats() {
Map<String, Object> arch = new LinkedHashMap<>();
Map<String, String> kindToLabel = Map.of(
"class", "classes", "interface", "interfaces",
"abstract_class", "abstract_classes", "enum", "enums",
"annotation_type", "annotation_types", "module", "modules",
"method", "methods");
List<String> 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<String, String> 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 <K> Map<K, Long> sortByValueDesc(Map<K, Long> map) {
return map.entrySet().stream()
.sorted(Map.Entry.<K, Long>comparingByValue().reversed())
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new));
}

// --- Internal helpers ---

/**
Expand Down
Loading
Loading