-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGraphCommand.java
More file actions
197 lines (169 loc) · 7.05 KB
/
Copy pathGraphCommand.java
File metadata and controls
197 lines (169 loc) · 7.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package io.github.randomcodespace.iq.cli;
import io.github.randomcodespace.iq.graph.GraphStore;
import io.github.randomcodespace.iq.model.CodeNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
/**
* Export the knowledge graph in various formats.
*/
@Component
@Command(name = "graph", mixinStandardHelpOptions = true,
description = "Export graph in various formats")
public class GraphCommand implements Callable<Integer> {
@Parameters(index = "0", defaultValue = ".", description = "Path to analyzed codebase")
private Path path;
@Option(names = {"--format", "-f"}, defaultValue = "json",
description = "Output format: json, yaml, mermaid, dot (default: json)")
private String format;
@Option(names = {"--output", "-o"}, description = "Output file (stdout if omitted)")
private Path output;
@Option(names = {"--max-nodes"}, defaultValue = "500",
description = "Maximum nodes to export (default: 500)")
private int maxNodes;
@Option(names = {"--focus"}, description = "Node ID to center export on")
private String focus;
@Option(names = {"--hops"}, defaultValue = "2",
description = "Hops from focus node (default: 2)")
private int hops;
private final GraphStore graphStore;
/** No-arg constructor for Picocli direct instantiation. */
public GraphCommand() {
this.graphStore = null;
}
@Autowired
public GraphCommand(Optional<GraphStore> graphStore) {
this.graphStore = graphStore.orElse(null);
}
/** Convenience constructor for tests. */
GraphCommand(GraphStore graphStore) {
this.graphStore = graphStore;
}
@Override
public Integer call() {
if (graphStore == null) {
CliOutput.error("Graph export requires the serve profile (Neo4j). Use 'code-iq serve' to start the server, or 'code-iq stats' for cache-based queries.");
return 1;
}
List<CodeNode> nodes;
if (focus != null && !focus.isBlank()) {
nodes = graphStore.findEgoGraph(focus, hops);
} else {
nodes = graphStore.findAllPaginated(0, maxNodes);
}
if (nodes.isEmpty()) {
CliOutput.warn("No graph data found. Run 'code-iq analyze' first.");
return 1;
}
String content = switch (format.toLowerCase()) {
case "yaml" -> renderYaml(nodes);
case "mermaid" -> renderMermaid(nodes);
case "dot" -> renderDot(nodes);
default -> renderJson(nodes);
};
if (output != null) {
try {
Files.writeString(output, content, StandardCharsets.UTF_8);
CliOutput.success("Graph exported to " + output);
} catch (IOException e) {
CliOutput.error("Failed to write output: " + e.getMessage());
return 1;
}
} else {
System.out.println(content);
}
return 0;
}
private String renderYaml(List<CodeNode> nodes) {
List<Map<String, Object>> nodeList = nodes.stream()
.map(n -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", n.getId());
m.put("kind", n.getKind().getValue());
m.put("label", n.getLabel());
return m;
})
.collect(Collectors.toList());
Map<String, Object> graphData = new LinkedHashMap<>();
graphData.put("nodes", nodeList);
graphData.put("count", nodes.size());
Yaml yaml = new Yaml(new org.yaml.snakeyaml.constructor.SafeConstructor(new org.yaml.snakeyaml.LoaderOptions()));
return yaml.dump(graphData);
}
private String renderJson(List<CodeNode> nodes) {
var sb = new StringBuilder();
sb.append("{\n \"nodes\": [\n");
for (int i = 0; i < nodes.size(); i++) {
CodeNode n = nodes.get(i);
sb.append(" {\"id\": \"").append(jsonEscape(n.getId()))
.append("\", \"kind\": \"").append(n.getKind().getValue())
.append("\", \"label\": \"").append(jsonEscape(n.getLabel()))
.append("\"}");
if (i < nodes.size() - 1) sb.append(",");
sb.append("\n");
}
sb.append(" ],\n \"count\": ").append(nodes.size()).append("\n}");
return sb.toString();
}
private String renderMermaid(List<CodeNode> nodes) {
var sb = new StringBuilder("graph TD\n");
var nodeIds = nodes.stream().map(CodeNode::getId).collect(Collectors.toSet());
for (CodeNode n : nodes) {
String safeId = mermaidId(n.getId());
sb.append(" ").append(safeId)
.append("[\"").append(n.getLabel()).append("\"]\n");
for (var edge : n.getEdges()) {
if (edge.getTarget() != null && nodeIds.contains(edge.getTarget().getId())) {
sb.append(" ").append(safeId)
.append(" -->|").append(edge.getKind().getValue())
.append("| ").append(mermaidId(edge.getTarget().getId()))
.append("\n");
}
}
}
return sb.toString();
}
private String renderDot(List<CodeNode> nodes) {
var sb = new StringBuilder("digraph G {\n rankdir=LR;\n");
var nodeIds = nodes.stream().map(CodeNode::getId).collect(Collectors.toSet());
for (CodeNode n : nodes) {
sb.append(" \"").append(dotEscape(n.getId()))
.append("\" [label=\"").append(dotEscape(n.getLabel()))
.append("\"];\n");
for (var edge : n.getEdges()) {
if (edge.getTarget() != null && nodeIds.contains(edge.getTarget().getId())) {
sb.append(" \"").append(dotEscape(n.getId()))
.append("\" -> \"").append(dotEscape(edge.getTarget().getId()))
.append("\" [label=\"").append(edge.getKind().getValue())
.append("\"];\n");
}
}
}
sb.append("}");
return sb.toString();
}
private static String jsonEscape(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
private static String dotEscape(String s) {
if (s == null) return "";
return s.replace("\"", "\\\"");
}
private static String mermaidId(String id) {
return id.replaceAll("[^a-zA-Z0-9_]", "_");
}
}