Skip to content
Merged
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
30 changes: 2 additions & 28 deletions src/main/java/io/github/randomcodespace/iq/cli/AnalyzeCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,34 +118,8 @@ public Integer call() {
+ nf.format(result.nodeCount()) + " nodes, "
+ nf.format(result.edgeCount()) + " edges in " + timeStr);
System.out.println();
CliOutput.info(" Files: " + nf.format(result.totalFiles()) + " discovered, "
+ nf.format(result.filesAnalyzed()) + " analyzed");
CliOutput.cyan(" Nodes: " + nf.format(result.nodeCount()));
CliOutput.cyan(" Edges: " + nf.format(result.edgeCount()));
CliOutput.info(" Time: " + timeStr);

if (!result.nodeBreakdown().isEmpty()) {
System.out.println();
StringBuilder topNodes = new StringBuilder(" Top node kinds: ");
result.nodeBreakdown().entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.forEach(e -> topNodes.append(e.getKey()).append(" (")
.append(nf.format(e.getValue())).append("), "));
if (topNodes.length() > 2) topNodes.setLength(topNodes.length() - 2);
CliOutput.info(topNodes.toString());
}

if (!result.languageBreakdown().isEmpty()) {
StringBuilder langs = new StringBuilder(" Languages: ");
result.languageBreakdown().entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.forEach(e -> langs.append(e.getKey()).append(" (")
.append(nf.format(e.getValue())).append("), "));
if (langs.length() > 2) langs.setLength(langs.length() - 2);
CliOutput.info(langs.toString());
}
CliOutput.printAnalysisStats(result, nf);
CliOutput.printBreakdowns(result, nf);

if (result.frameworkBreakdown() != null && !result.frameworkBreakdown().isEmpty()) {
StringBuilder fws = new StringBuilder(" Frameworks: ");
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.github.randomcodespace.iq.cli;

import io.github.randomcodespace.iq.analyzer.AnalysisResult;
import picocli.CommandLine;

import java.io.PrintStream;
import java.text.NumberFormat;
import java.util.Map;

/**
* Utility class for rich ANSI-colored CLI output.
Expand Down Expand Up @@ -76,6 +79,50 @@
return ANSI.string(ansiFormatted);
}

/**
* Print files/nodes/edges/time summary lines shared by analyze and index commands.
* Callers are responsible for printing the preceding success banner and any
* command-specific extra lines (e.g. "Store: H2…").
*/
static void printAnalysisStats(AnalysisResult result, NumberFormat nf) {
long secs = result.elapsed().toSeconds();
String timeStr = secs > 0 ? secs + "s" : result.elapsed().toMillis() + "ms";
info(" Files: " + nf.format(result.totalFiles()) + " discovered, "
+ nf.format(result.filesAnalyzed()) + " analyzed");
cyan(" Nodes: " + nf.format(result.nodeCount()));
cyan(" Edges: " + nf.format(result.edgeCount()));
info(" Time: " + timeStr);
}

/**
* Print node-kind and language breakdown lines shared by analyze and index commands.
* Prints a blank line before the node breakdown when it is non-empty.
*/
static void printBreakdowns(AnalysisResult result, NumberFormat nf) {
if (!result.nodeBreakdown().isEmpty()) {
System.out.println();

Check warning on line 103 in src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this use of System.out by a logger.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1ZED1rV80h1QdmRnMM&open=AZ1ZED1rV80h1QdmRnMM&pullRequest=37
StringBuilder topNodes = new StringBuilder(" Top node kinds: ");
result.nodeBreakdown().entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.forEach(e -> topNodes.append(e.getKey()).append(" (")
.append(nf.format(e.getValue())).append("), "));
if (topNodes.length() > 2) topNodes.setLength(topNodes.length() - 2);
info(topNodes.toString());
}

if (!result.languageBreakdown().isEmpty()) {
StringBuilder langs = new StringBuilder(" Languages: ");
result.languageBreakdown().entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.forEach(e -> langs.append(e.getKey()).append(" (")
.append(nf.format(e.getValue())).append("), "));
if (langs.length() > 2) langs.setLength(langs.length() - 2);
info(langs.toString());
}
}

/**
* Escape pipe characters that would break picocli ANSI markup.
*/
Expand Down
31 changes: 2 additions & 29 deletions src/main/java/io/github/randomcodespace/iq/cli/IndexCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import java.nio.file.Path;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;

/**
Expand Down Expand Up @@ -127,35 +126,9 @@ public Integer call() {
+ nf.format(result.nodeCount()) + " nodes, "
+ nf.format(result.edgeCount()) + " edges in " + timeStr);
System.out.println();
CliOutput.info(" Files: " + nf.format(result.totalFiles()) + " discovered, "
+ nf.format(result.filesAnalyzed()) + " analyzed");
CliOutput.cyan(" Nodes: " + nf.format(result.nodeCount()));
CliOutput.cyan(" Edges: " + nf.format(result.edgeCount()));
CliOutput.info(" Time: " + timeStr);
CliOutput.printAnalysisStats(result, nf);
CliOutput.info(" Store: H2 (.code-intelligence/analysis-cache)");

if (!result.nodeBreakdown().isEmpty()) {
System.out.println();
StringBuilder topNodes = new StringBuilder(" Top node kinds: ");
result.nodeBreakdown().entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.forEach(e -> topNodes.append(e.getKey()).append(" (")
.append(nf.format(e.getValue())).append("), "));
if (topNodes.length() > 2) topNodes.setLength(topNodes.length() - 2);
CliOutput.info(topNodes.toString());
}

if (!result.languageBreakdown().isEmpty()) {
StringBuilder langs = new StringBuilder(" Languages: ");
result.languageBreakdown().entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.forEach(e -> langs.append(e.getKey()).append(" (")
.append(nf.format(e.getValue())).append("), "));
if (langs.length() > 2) langs.setLength(langs.length() - 2);
CliOutput.info(langs.toString());
}
CliOutput.printBreakdowns(result, nf);

System.out.println();
CliOutput.info(" Next step: code-iq enrich " + root);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package io.github.randomcodespace.iq.detector;

import io.github.randomcodespace.iq.model.CodeEdge;
import io.github.randomcodespace.iq.model.CodeNode;
import io.github.randomcodespace.iq.model.EdgeKind;
import io.github.randomcodespace.iq.model.NodeKind;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -87,4 +93,40 @@ protected int getInt(Object container, String key, int defaultValue) {
}
return defaultValue;
}

/**
* Build a CONFIG_FILE node for the given context and format.
* The returned node is NOT added to any list — callers must do that themselves.
*/
protected CodeNode buildFileNode(DetectorContext ctx, String format) {
String fp = ctx.filePath();
String fileId = format + ":" + fp;
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
fileNode.setFqn(fp);
fileNode.setModule(ctx.moduleName());
fileNode.setFilePath(fp);
fileNode.setLineStart(1);
fileNode.setProperties(new HashMap<>(Map.of("format", format)));
return fileNode;
}

/**
* Build a CONFIG_KEY node and a CONTAINS edge from {@code fileId} to it,
* appending both to the supplied lists.
*/
protected void addKeyNode(String fileId, String fp, String key, String format,
DetectorContext ctx, List<CodeNode> nodes, List<CodeEdge> edges) {
String keyId = format + ":" + fp + ":" + key;
CodeNode keyNode = new CodeNode(keyId, NodeKind.CONFIG_KEY, key);
keyNode.setFqn(fp + ":" + key);
keyNode.setModule(ctx.moduleName());
keyNode.setFilePath(fp);
nodes.add(keyNode);
CodeEdge edge = new CodeEdge();
edge.setId(fileId + "->" + keyId);
edge.setKind(EdgeKind.CONTAINS);
edge.setSourceId(fileId);
edge.setTarget(new CodeNode(keyId, null, null));
edges.add(edge);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.github.randomcodespace.iq.detector;

import io.github.randomcodespace.iq.analyzer.InfraEndpoint;
import io.github.randomcodespace.iq.analyzer.InfrastructureRegistry;
import io.github.randomcodespace.iq.model.CodeEdge;
import io.github.randomcodespace.iq.model.CodeNode;
import io.github.randomcodespace.iq.model.EdgeKind;
import io.github.randomcodespace.iq.model.NodeKind;

import java.util.List;

/**
* Shared helper for detectors that emit DATABASE_CONNECTION nodes and CONNECTS_TO edges.
* Used by Python, TypeScript, and Java ORM/database detectors.
*/
public final class DetectorDbHelper {

private DetectorDbHelper() {}

/**
* Ensure a DATABASE_CONNECTION node exists in the result, creating it if needed.
* Uses the first database from the InfrastructureRegistry if available,
* otherwise creates a generic "database:unknown" node.
*
* @param registry infrastructure registry (may be null)
* @param nodes the nodes list to add the DB node to if missing
* @return the database node ID
*/
public static String ensureDbNode(InfrastructureRegistry registry, List<CodeNode> nodes) {
String dbNodeId;
if (registry != null && !registry.getDatabases().isEmpty()) {
InfraEndpoint db = registry.getDatabases().values().iterator().next();
dbNodeId = "infra:" + db.id();
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
CodeNode dbNode = new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION,
db.name() + " (" + db.type() + ")");
dbNode.getProperties().put("type", db.type());
if (db.connectionUrl() != null) dbNode.getProperties().put("url", db.connectionUrl());
nodes.add(dbNode);
}
} else {
dbNodeId = "database:unknown";
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
nodes.add(new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
}
}
return dbNodeId;
}

/**
* Add a CONNECTS_TO edge from the given source node to the database node.
*
* @param sourceId the source node ID
* @param registry infrastructure registry (may be null)
* @param nodes the nodes list (used to find/create the DB node)
* @param edges the edges list to add the edge to
*/
public static void addDbEdge(String sourceId, InfrastructureRegistry registry,
List<CodeNode> nodes, List<CodeEdge> edges) {
String dbNodeId = ensureDbNode(registry, nodes);
CodeNode targetRef = nodes.stream()
.filter(n -> dbNodeId.equals(n.getId()))
.findFirst()
.orElseGet(() -> new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
CodeEdge edge = new CodeEdge();
edge.setId(sourceId + "->connects_to->" + dbNodeId);
edge.setKind(EdgeKind.CONNECTS_TO);
edge.setSourceId(sourceId);
edge.setTarget(targetRef);
edges.add(edge);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,7 @@ public DetectorResult detect(DetectorContext ctx) {
List<CodeEdge> edges = new ArrayList<>();

// CONFIG_FILE node for the file itself
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
fileNode.setFqn(fp);
fileNode.setModule(ctx.moduleName());
fileNode.setFilePath(fp);
fileNode.setLineStart(1);
fileNode.setProperties(Map.of("format", "ini"));
nodes.add(fileNode);
nodes.add(buildFileNode(ctx, "ini"));

Object parsedData = ctx.parsedData();
if (parsedData == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,7 @@ public DetectorResult detect(DetectorContext ctx) {
List<CodeEdge> edges = new ArrayList<>();

// CONFIG_FILE node for the file itself
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
fileNode.setFqn(fp);
fileNode.setModule(ctx.moduleName());
fileNode.setFilePath(fp);
fileNode.setLineStart(1);
fileNode.setProperties(Map.of("format", "json"));
nodes.add(fileNode);
nodes.add(buildFileNode(ctx, "json"));

// Extract data from parsed_data
Object parsedData = ctx.parsedData();
Expand All @@ -71,20 +65,7 @@ public DetectorResult detect(DetectorContext ctx) {
}

for (String key : data.keySet()) {
String keyId = "json:" + fp + ":" + key;

CodeNode keyNode = new CodeNode(keyId, NodeKind.CONFIG_KEY, key);
keyNode.setFqn(fp + ":" + key);
keyNode.setModule(ctx.moduleName());
keyNode.setFilePath(fp);
nodes.add(keyNode);

CodeEdge edge = new CodeEdge();
edge.setId(fileId + "->" + keyId);
edge.setKind(EdgeKind.CONTAINS);
edge.setSourceId(fileId);
edge.setTarget(new CodeNode(keyId, null, null));
edges.add(edge);
addKeyNode(fileId, fp, key, "json", ctx, nodes, edges);
}

return DetectorResult.of(nodes, edges);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;

import io.github.randomcodespace.iq.detector.DetectorInfo;
import io.github.randomcodespace.iq.detector.ParserType;

Expand Down Expand Up @@ -52,13 +53,7 @@ public DetectorResult detect(DetectorContext ctx) {
List<CodeEdge> edges = new ArrayList<>();

// CONFIG_FILE node for the file itself
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
fileNode.setFqn(fp);
fileNode.setModule(ctx.moduleName());
fileNode.setFilePath(fp);
fileNode.setLineStart(1);
fileNode.setProperties(Map.of("format", "toml"));
nodes.add(fileNode);
nodes.add(buildFileNode(ctx, "toml"));

Object parsedData = ctx.parsedData();
if (parsedData == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,7 @@ public DetectorResult detect(DetectorContext ctx) {
List<CodeEdge> edges = new ArrayList<>();

// CONFIG_FILE node for the file itself
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
fileNode.setFqn(fp);
fileNode.setModule(ctx.moduleName());
fileNode.setFilePath(fp);
fileNode.setLineStart(1);
fileNode.setProperties(Map.of("format", "yaml"));
nodes.add(fileNode);
nodes.add(buildFileNode(ctx, "yaml"));

Object parsedData = ctx.parsedData();
if (parsedData == null) {
Expand Down Expand Up @@ -84,20 +78,7 @@ public DetectorResult detect(DetectorContext ctx) {
}

for (String keyStr : topLevelKeys) {
String keyId = "yaml:" + fp + ":" + keyStr;

CodeNode keyNode = new CodeNode(keyId, NodeKind.CONFIG_KEY, keyStr);
keyNode.setFqn(fp + ":" + keyStr);
keyNode.setModule(ctx.moduleName());
keyNode.setFilePath(fp);
nodes.add(keyNode);

CodeEdge edge = new CodeEdge();
edge.setId(fileId + "->" + keyId);
edge.setKind(EdgeKind.CONTAINS);
edge.setSourceId(fileId);
edge.setTarget(new CodeNode(keyId, null, null));
edges.add(edge);
addKeyNode(fileId, fp, keyStr, "yaml", ctx, nodes, edges);
}

return DetectorResult.of(nodes, edges);
Expand Down
Loading
Loading