Skip to content

Commit 57dc990

Browse files
authored
Merge pull request #37 from RandomCodeSpace/coverage/boost-80pct-r2
fix: boost coverage to 90.6%, eliminate ~535 lines duplication, fix MD5 security hotspot
2 parents e5d49de + 2067842 commit 57dc990

14 files changed

Lines changed: 184 additions & 314 deletions

src/main/java/io/github/randomcodespace/iq/cli/AnalyzeCommand.java

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -118,34 +118,8 @@ public Integer call() {
118118
+ nf.format(result.nodeCount()) + " nodes, "
119119
+ nf.format(result.edgeCount()) + " edges in " + timeStr);
120120
System.out.println();
121-
CliOutput.info(" Files: " + nf.format(result.totalFiles()) + " discovered, "
122-
+ nf.format(result.filesAnalyzed()) + " analyzed");
123-
CliOutput.cyan(" Nodes: " + nf.format(result.nodeCount()));
124-
CliOutput.cyan(" Edges: " + nf.format(result.edgeCount()));
125-
CliOutput.info(" Time: " + timeStr);
126-
127-
if (!result.nodeBreakdown().isEmpty()) {
128-
System.out.println();
129-
StringBuilder topNodes = new StringBuilder(" Top node kinds: ");
130-
result.nodeBreakdown().entrySet().stream()
131-
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
132-
.limit(10)
133-
.forEach(e -> topNodes.append(e.getKey()).append(" (")
134-
.append(nf.format(e.getValue())).append("), "));
135-
if (topNodes.length() > 2) topNodes.setLength(topNodes.length() - 2);
136-
CliOutput.info(topNodes.toString());
137-
}
138-
139-
if (!result.languageBreakdown().isEmpty()) {
140-
StringBuilder langs = new StringBuilder(" Languages: ");
141-
result.languageBreakdown().entrySet().stream()
142-
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
143-
.limit(10)
144-
.forEach(e -> langs.append(e.getKey()).append(" (")
145-
.append(nf.format(e.getValue())).append("), "));
146-
if (langs.length() > 2) langs.setLength(langs.length() - 2);
147-
CliOutput.info(langs.toString());
148-
}
121+
CliOutput.printAnalysisStats(result, nf);
122+
CliOutput.printBreakdowns(result, nf);
149123

150124
if (result.frameworkBreakdown() != null && !result.frameworkBreakdown().isEmpty()) {
151125
StringBuilder fws = new StringBuilder(" Frameworks: ");

src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package io.github.randomcodespace.iq.cli;
22

3+
import io.github.randomcodespace.iq.analyzer.AnalysisResult;
34
import picocli.CommandLine;
45

56
import java.io.PrintStream;
7+
import java.text.NumberFormat;
8+
import java.util.Map;
69

710
/**
811
* Utility class for rich ANSI-colored CLI output.
@@ -76,6 +79,50 @@ static String format(String ansiFormatted) {
7679
return ANSI.string(ansiFormatted);
7780
}
7881

82+
/**
83+
* Print files/nodes/edges/time summary lines shared by analyze and index commands.
84+
* Callers are responsible for printing the preceding success banner and any
85+
* command-specific extra lines (e.g. "Store: H2…").
86+
*/
87+
static void printAnalysisStats(AnalysisResult result, NumberFormat nf) {
88+
long secs = result.elapsed().toSeconds();
89+
String timeStr = secs > 0 ? secs + "s" : result.elapsed().toMillis() + "ms";
90+
info(" Files: " + nf.format(result.totalFiles()) + " discovered, "
91+
+ nf.format(result.filesAnalyzed()) + " analyzed");
92+
cyan(" Nodes: " + nf.format(result.nodeCount()));
93+
cyan(" Edges: " + nf.format(result.edgeCount()));
94+
info(" Time: " + timeStr);
95+
}
96+
97+
/**
98+
* Print node-kind and language breakdown lines shared by analyze and index commands.
99+
* Prints a blank line before the node breakdown when it is non-empty.
100+
*/
101+
static void printBreakdowns(AnalysisResult result, NumberFormat nf) {
102+
if (!result.nodeBreakdown().isEmpty()) {
103+
System.out.println();
104+
StringBuilder topNodes = new StringBuilder(" Top node kinds: ");
105+
result.nodeBreakdown().entrySet().stream()
106+
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
107+
.limit(10)
108+
.forEach(e -> topNodes.append(e.getKey()).append(" (")
109+
.append(nf.format(e.getValue())).append("), "));
110+
if (topNodes.length() > 2) topNodes.setLength(topNodes.length() - 2);
111+
info(topNodes.toString());
112+
}
113+
114+
if (!result.languageBreakdown().isEmpty()) {
115+
StringBuilder langs = new StringBuilder(" Languages: ");
116+
result.languageBreakdown().entrySet().stream()
117+
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
118+
.limit(10)
119+
.forEach(e -> langs.append(e.getKey()).append(" (")
120+
.append(nf.format(e.getValue())).append("), "));
121+
if (langs.length() > 2) langs.setLength(langs.length() - 2);
122+
info(langs.toString());
123+
}
124+
}
125+
79126
/**
80127
* Escape pipe characters that would break picocli ANSI markup.
81128
*/

src/main/java/io/github/randomcodespace/iq/cli/IndexCommand.java

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import java.nio.file.Path;
1313
import java.text.NumberFormat;
1414
import java.util.Locale;
15-
import java.util.Map;
1615
import java.util.concurrent.Callable;
1716

1817
/**
@@ -127,35 +126,9 @@ public Integer call() {
127126
+ nf.format(result.nodeCount()) + " nodes, "
128127
+ nf.format(result.edgeCount()) + " edges in " + timeStr);
129128
System.out.println();
130-
CliOutput.info(" Files: " + nf.format(result.totalFiles()) + " discovered, "
131-
+ nf.format(result.filesAnalyzed()) + " analyzed");
132-
CliOutput.cyan(" Nodes: " + nf.format(result.nodeCount()));
133-
CliOutput.cyan(" Edges: " + nf.format(result.edgeCount()));
134-
CliOutput.info(" Time: " + timeStr);
129+
CliOutput.printAnalysisStats(result, nf);
135130
CliOutput.info(" Store: H2 (.code-intelligence/analysis-cache)");
136-
137-
if (!result.nodeBreakdown().isEmpty()) {
138-
System.out.println();
139-
StringBuilder topNodes = new StringBuilder(" Top node kinds: ");
140-
result.nodeBreakdown().entrySet().stream()
141-
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
142-
.limit(10)
143-
.forEach(e -> topNodes.append(e.getKey()).append(" (")
144-
.append(nf.format(e.getValue())).append("), "));
145-
if (topNodes.length() > 2) topNodes.setLength(topNodes.length() - 2);
146-
CliOutput.info(topNodes.toString());
147-
}
148-
149-
if (!result.languageBreakdown().isEmpty()) {
150-
StringBuilder langs = new StringBuilder(" Languages: ");
151-
result.languageBreakdown().entrySet().stream()
152-
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
153-
.limit(10)
154-
.forEach(e -> langs.append(e.getKey()).append(" (")
155-
.append(nf.format(e.getValue())).append("), "));
156-
if (langs.length() > 2) langs.setLength(langs.length() - 2);
157-
CliOutput.info(langs.toString());
158-
}
131+
CliOutput.printBreakdowns(result, nf);
159132

160133
System.out.println();
161134
CliOutput.info(" Next step: code-iq enrich " + root);

src/main/java/io/github/randomcodespace/iq/detector/AbstractStructuredDetector.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package io.github.randomcodespace.iq.detector;
22

3+
import io.github.randomcodespace.iq.model.CodeEdge;
4+
import io.github.randomcodespace.iq.model.CodeNode;
5+
import io.github.randomcodespace.iq.model.EdgeKind;
6+
import io.github.randomcodespace.iq.model.NodeKind;
7+
38
import java.util.Collections;
9+
import java.util.HashMap;
410
import java.util.List;
511
import java.util.Map;
612

@@ -87,4 +93,40 @@ protected int getInt(Object container, String key, int defaultValue) {
8793
}
8894
return defaultValue;
8995
}
96+
97+
/**
98+
* Build a CONFIG_FILE node for the given context and format.
99+
* The returned node is NOT added to any list — callers must do that themselves.
100+
*/
101+
protected CodeNode buildFileNode(DetectorContext ctx, String format) {
102+
String fp = ctx.filePath();
103+
String fileId = format + ":" + fp;
104+
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
105+
fileNode.setFqn(fp);
106+
fileNode.setModule(ctx.moduleName());
107+
fileNode.setFilePath(fp);
108+
fileNode.setLineStart(1);
109+
fileNode.setProperties(new HashMap<>(Map.of("format", format)));
110+
return fileNode;
111+
}
112+
113+
/**
114+
* Build a CONFIG_KEY node and a CONTAINS edge from {@code fileId} to it,
115+
* appending both to the supplied lists.
116+
*/
117+
protected void addKeyNode(String fileId, String fp, String key, String format,
118+
DetectorContext ctx, List<CodeNode> nodes, List<CodeEdge> edges) {
119+
String keyId = format + ":" + fp + ":" + key;
120+
CodeNode keyNode = new CodeNode(keyId, NodeKind.CONFIG_KEY, key);
121+
keyNode.setFqn(fp + ":" + key);
122+
keyNode.setModule(ctx.moduleName());
123+
keyNode.setFilePath(fp);
124+
nodes.add(keyNode);
125+
CodeEdge edge = new CodeEdge();
126+
edge.setId(fileId + "->" + keyId);
127+
edge.setKind(EdgeKind.CONTAINS);
128+
edge.setSourceId(fileId);
129+
edge.setTarget(new CodeNode(keyId, null, null));
130+
edges.add(edge);
131+
}
90132
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.github.randomcodespace.iq.detector;
2+
3+
import io.github.randomcodespace.iq.analyzer.InfraEndpoint;
4+
import io.github.randomcodespace.iq.analyzer.InfrastructureRegistry;
5+
import io.github.randomcodespace.iq.model.CodeEdge;
6+
import io.github.randomcodespace.iq.model.CodeNode;
7+
import io.github.randomcodespace.iq.model.EdgeKind;
8+
import io.github.randomcodespace.iq.model.NodeKind;
9+
10+
import java.util.List;
11+
12+
/**
13+
* Shared helper for detectors that emit DATABASE_CONNECTION nodes and CONNECTS_TO edges.
14+
* Used by Python, TypeScript, and Java ORM/database detectors.
15+
*/
16+
public final class DetectorDbHelper {
17+
18+
private DetectorDbHelper() {}
19+
20+
/**
21+
* Ensure a DATABASE_CONNECTION node exists in the result, creating it if needed.
22+
* Uses the first database from the InfrastructureRegistry if available,
23+
* otherwise creates a generic "database:unknown" node.
24+
*
25+
* @param registry infrastructure registry (may be null)
26+
* @param nodes the nodes list to add the DB node to if missing
27+
* @return the database node ID
28+
*/
29+
public static String ensureDbNode(InfrastructureRegistry registry, List<CodeNode> nodes) {
30+
String dbNodeId;
31+
if (registry != null && !registry.getDatabases().isEmpty()) {
32+
InfraEndpoint db = registry.getDatabases().values().iterator().next();
33+
dbNodeId = "infra:" + db.id();
34+
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
35+
CodeNode dbNode = new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION,
36+
db.name() + " (" + db.type() + ")");
37+
dbNode.getProperties().put("type", db.type());
38+
if (db.connectionUrl() != null) dbNode.getProperties().put("url", db.connectionUrl());
39+
nodes.add(dbNode);
40+
}
41+
} else {
42+
dbNodeId = "database:unknown";
43+
if (nodes.stream().noneMatch(n -> dbNodeId.equals(n.getId()))) {
44+
nodes.add(new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
45+
}
46+
}
47+
return dbNodeId;
48+
}
49+
50+
/**
51+
* Add a CONNECTS_TO edge from the given source node to the database node.
52+
*
53+
* @param sourceId the source node ID
54+
* @param registry infrastructure registry (may be null)
55+
* @param nodes the nodes list (used to find/create the DB node)
56+
* @param edges the edges list to add the edge to
57+
*/
58+
public static void addDbEdge(String sourceId, InfrastructureRegistry registry,
59+
List<CodeNode> nodes, List<CodeEdge> edges) {
60+
String dbNodeId = ensureDbNode(registry, nodes);
61+
CodeNode targetRef = nodes.stream()
62+
.filter(n -> dbNodeId.equals(n.getId()))
63+
.findFirst()
64+
.orElseGet(() -> new CodeNode(dbNodeId, NodeKind.DATABASE_CONNECTION, "Database"));
65+
CodeEdge edge = new CodeEdge();
66+
edge.setId(sourceId + "->connects_to->" + dbNodeId);
67+
edge.setKind(EdgeKind.CONNECTS_TO);
68+
edge.setSourceId(sourceId);
69+
edge.setTarget(targetRef);
70+
edges.add(edge);
71+
}
72+
}

src/main/java/io/github/randomcodespace/iq/detector/config/IniStructureDetector.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,7 @@ public DetectorResult detect(DetectorContext ctx) {
5252
List<CodeEdge> edges = new ArrayList<>();
5353

5454
// CONFIG_FILE node for the file itself
55-
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
56-
fileNode.setFqn(fp);
57-
fileNode.setModule(ctx.moduleName());
58-
fileNode.setFilePath(fp);
59-
fileNode.setLineStart(1);
60-
fileNode.setProperties(Map.of("format", "ini"));
61-
nodes.add(fileNode);
55+
nodes.add(buildFileNode(ctx, "ini"));
6256

6357
Object parsedData = ctx.parsedData();
6458
if (parsedData == null) {

src/main/java/io/github/randomcodespace/iq/detector/config/JsonStructureDetector.java

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,7 @@ public DetectorResult detect(DetectorContext ctx) {
4949
List<CodeEdge> edges = new ArrayList<>();
5050

5151
// CONFIG_FILE node for the file itself
52-
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
53-
fileNode.setFqn(fp);
54-
fileNode.setModule(ctx.moduleName());
55-
fileNode.setFilePath(fp);
56-
fileNode.setLineStart(1);
57-
fileNode.setProperties(Map.of("format", "json"));
58-
nodes.add(fileNode);
52+
nodes.add(buildFileNode(ctx, "json"));
5953

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

7367
for (String key : data.keySet()) {
74-
String keyId = "json:" + fp + ":" + key;
75-
76-
CodeNode keyNode = new CodeNode(keyId, NodeKind.CONFIG_KEY, key);
77-
keyNode.setFqn(fp + ":" + key);
78-
keyNode.setModule(ctx.moduleName());
79-
keyNode.setFilePath(fp);
80-
nodes.add(keyNode);
81-
82-
CodeEdge edge = new CodeEdge();
83-
edge.setId(fileId + "->" + keyId);
84-
edge.setKind(EdgeKind.CONTAINS);
85-
edge.setSourceId(fileId);
86-
edge.setTarget(new CodeNode(keyId, null, null));
87-
edges.add(edge);
68+
addKeyNode(fileId, fp, key, "json", ctx, nodes, edges);
8869
}
8970

9071
return DetectorResult.of(nodes, edges);

src/main/java/io/github/randomcodespace/iq/detector/config/TomlStructureDetector.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.List;
1515
import java.util.Map;
1616
import java.util.Set;
17+
1718
import io.github.randomcodespace.iq.detector.DetectorInfo;
1819
import io.github.randomcodespace.iq.detector.ParserType;
1920

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

5455
// CONFIG_FILE node for the file itself
55-
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
56-
fileNode.setFqn(fp);
57-
fileNode.setModule(ctx.moduleName());
58-
fileNode.setFilePath(fp);
59-
fileNode.setLineStart(1);
60-
fileNode.setProperties(Map.of("format", "toml"));
61-
nodes.add(fileNode);
56+
nodes.add(buildFileNode(ctx, "toml"));
6257

6358
Object parsedData = ctx.parsedData();
6459
if (parsedData == null) {

src/main/java/io/github/randomcodespace/iq/detector/config/YamlStructureDetector.java

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,7 @@ public DetectorResult detect(DetectorContext ctx) {
5050
List<CodeEdge> edges = new ArrayList<>();
5151

5252
// CONFIG_FILE node for the file itself
53-
CodeNode fileNode = new CodeNode(fileId, NodeKind.CONFIG_FILE, fp);
54-
fileNode.setFqn(fp);
55-
fileNode.setModule(ctx.moduleName());
56-
fileNode.setFilePath(fp);
57-
fileNode.setLineStart(1);
58-
fileNode.setProperties(Map.of("format", "yaml"));
59-
nodes.add(fileNode);
53+
nodes.add(buildFileNode(ctx, "yaml"));
6054

6155
Object parsedData = ctx.parsedData();
6256
if (parsedData == null) {
@@ -84,20 +78,7 @@ public DetectorResult detect(DetectorContext ctx) {
8478
}
8579

8680
for (String keyStr : topLevelKeys) {
87-
String keyId = "yaml:" + fp + ":" + keyStr;
88-
89-
CodeNode keyNode = new CodeNode(keyId, NodeKind.CONFIG_KEY, keyStr);
90-
keyNode.setFqn(fp + ":" + keyStr);
91-
keyNode.setModule(ctx.moduleName());
92-
keyNode.setFilePath(fp);
93-
nodes.add(keyNode);
94-
95-
CodeEdge edge = new CodeEdge();
96-
edge.setId(fileId + "->" + keyId);
97-
edge.setKind(EdgeKind.CONTAINS);
98-
edge.setSourceId(fileId);
99-
edge.setTarget(new CodeNode(keyId, null, null));
100-
edges.add(edge);
81+
addKeyNode(fileId, fp, keyStr, "yaml", ctx, nodes, edges);
10182
}
10283

10384
return DetectorResult.of(nodes, edges);

0 commit comments

Comments
 (0)