001package gudusoft.gsqlparser.analyzer.v2;
002
003import gudusoft.gsqlparser.analyzer.v2.callgraph.CallEdge;
004import gudusoft.gsqlparser.analyzer.v2.callgraph.CallGraph;
005import gudusoft.gsqlparser.analyzer.v2.callgraph.CallGraphNode;
006import gudusoft.gsqlparser.analyzer.v2.callgraph.ExternalCallEdge;
007import gudusoft.gsqlparser.analyzer.v2.callgraph.TableAccess;
008import gudusoft.gsqlparser.ir.bound.BoundProgram;
009import gudusoft.gsqlparser.ir.bound.BoundRoutineRef;
010import gudusoft.gsqlparser.ir.bound.BoundRoutineSymbol;
011import gudusoft.gsqlparser.ir.common.SourceAnchor;
012
013import java.util.List;
014import java.util.Map;
015
016/**
017 * Exports IR analysis results to JSON format.
018 * <p>
019 * Phase A provides basic BoundProgram export.
020 * Phase B adds CallGraph and impact analysis export.
021 */
022public class AnalyzerJsonExporter {
023
024    private AnalyzerJsonExporter() {}
025
026    /** Whether to include source anchors in output. */
027    private static boolean includeSourceAnchors = true;
028    /** Whether to include evidence in output. */
029    private static boolean includeEvidence = true;
030
031    /**
032     * Exports the full IRProgram to JSON (no call graph).
033     */
034    public static String export(IRProgram program) {
035        return export(program, null);
036    }
037
038    /**
039     * Exports the full IRProgram and optional CallGraph to JSON.
040     */
041    public static String export(IRProgram program, CallGraph callGraph) {
042        return export(program, callGraph, null);
043    }
044
045    /**
046     * Exports the full IRProgram and optional CallGraph to JSON, with config control.
047     */
048    public static String export(IRProgram program, CallGraph callGraph, AnalyzerV2Config config) {
049        if (program == null) {
050            return "{}";
051        }
052        // Apply config
053        if (config != null) {
054            includeSourceAnchors = config.includeSourceAnchors;
055            includeEvidence = config.includeEvidence;
056        } else {
057            includeSourceAnchors = true;
058            includeEvidence = true;
059        }
060
061        StringBuilder sb = new StringBuilder();
062        sb.append("{\n");
063        exportBoundProgram(sb, program.getBoundProgram(), "  ");
064        if (callGraph != null) {
065            sb.append(",\n");
066            exportCallGraph(sb, callGraph, "  ");
067        }
068        sb.append("}\n");
069        return sb.toString();
070    }
071
072    private static void exportBoundProgram(StringBuilder sb, BoundProgram bound, String indent) {
073        if (bound == null) {
074            sb.append(indent).append("\"boundProgram\": null\n");
075            return;
076        }
077
078        sb.append(indent).append("\"boundProgram\": {\n");
079        String inner = indent + "  ";
080
081        // Routine index
082        sb.append(inner).append("\"routines\": [");
083        boolean first = true;
084        for (Map.Entry<String, BoundRoutineSymbol> entry : bound.getRoutineIndex().entrySet()) {
085            if (!first) sb.append(",");
086            first = false;
087            sb.append("\n");
088            exportRoutineSymbol(sb, entry.getValue(), inner + "  ");
089        }
090        if (!bound.getRoutineIndex().isEmpty()) {
091            sb.append("\n").append(inner);
092        }
093        sb.append("],\n");
094
095        // Object refs
096        sb.append(inner).append("\"objectRefs\": ").append(bound.getAllObjectRefs().size()).append(",\n");
097
098        // Column refs
099        sb.append(inner).append("\"columnRefs\": ").append(bound.getAllColumnRefs().size()).append(",\n");
100
101        // Routine refs
102        sb.append(inner).append("\"routineRefs\": [");
103        first = true;
104        for (BoundRoutineRef ref : bound.getAllRoutineRefs()) {
105            if (!first) sb.append(",");
106            first = false;
107            sb.append("\n");
108            exportRoutineRef(sb, ref, inner + "  ");
109        }
110        if (!bound.getAllRoutineRefs().isEmpty()) {
111            sb.append("\n").append(inner);
112        }
113        sb.append("]\n");
114
115        sb.append(indent).append("}");
116    }
117
118    private static void exportCallGraph(StringBuilder sb, CallGraph callGraph, String indent) {
119        sb.append(indent).append("\"callGraph\": {\n");
120        String inner = indent + "  ";
121
122        // Nodes
123        sb.append(inner).append("\"nodes\": [");
124        boolean first = true;
125        for (Map.Entry<String, CallGraphNode> entry : callGraph.getNodes().entrySet()) {
126            if (!first) sb.append(",");
127            first = false;
128            sb.append("\n");
129            exportCallGraphNode(sb, entry.getValue(), inner + "  ");
130        }
131        if (!callGraph.getNodes().isEmpty()) {
132            sb.append("\n").append(inner);
133        }
134        sb.append("],\n");
135
136        // Edges
137        sb.append(inner).append("\"edges\": [");
138        first = true;
139        for (CallEdge edge : callGraph.getEdges()) {
140            if (!first) sb.append(",");
141            first = false;
142            sb.append("\n");
143            exportCallEdge(sb, edge, inner + "  ");
144        }
145        if (!callGraph.getEdges().isEmpty()) {
146            sb.append("\n").append(inner);
147        }
148        sb.append("]");
149
150        // External dependencies
151        if (!callGraph.getExternalEdges().isEmpty()) {
152            sb.append(",\n");
153            exportExternalDependencies(sb, callGraph, inner);
154        } else {
155            sb.append("\n");
156        }
157
158        sb.append(indent).append("}");
159    }
160
161    private static void exportCallGraphNode(StringBuilder sb, CallGraphNode node, String indent) {
162        sb.append(indent).append("{");
163        sb.append("\"routineId\": \"").append(escapeJson(node.getRoutineId())).append("\"");
164        sb.append(", \"name\": \"").append(escapeJson(node.getRoutineName())).append("\"");
165        if (node.getPackageName() != null) {
166            sb.append(", \"package\": \"").append(escapeJson(node.getPackageName())).append("\"");
167        }
168        sb.append(", \"kind\": \"").append(node.getRoutineKind()).append("\"");
169
170        if (!node.getTableAccesses().isEmpty()) {
171            sb.append(", \"tableAccesses\": [");
172            boolean first = true;
173            for (TableAccess ta : node.getTableAccesses()) {
174                if (!first) sb.append(", ");
175                first = false;
176                sb.append("{\"table\": \"").append(escapeJson(ta.getTableName())).append("\"");
177                sb.append(", \"access\": \"").append(ta.getAccessKind()).append("\"}");
178            }
179            sb.append("]");
180        }
181        sb.append("}");
182    }
183
184    private static void exportCallEdge(StringBuilder sb, CallEdge edge, String indent) {
185        sb.append(indent).append("{");
186        sb.append("\"caller\": \"").append(escapeJson(edge.getCallerRoutineId())).append("\"");
187        sb.append(", \"callee\": \"").append(escapeJson(edge.getCalleeRoutineId())).append("\"");
188        if (includeSourceAnchors && edge.getCallSiteAnchor() != null) {
189            sb.append(", \"callSite\": ");
190            exportSourceAnchor(sb, edge.getCallSiteAnchor());
191        }
192        sb.append("}");
193    }
194
195    private static void exportRoutineSymbol(StringBuilder sb, BoundRoutineSymbol symbol, String indent) {
196        sb.append(indent).append("{");
197        sb.append("\"routineId\": \"").append(escapeJson(symbol.getRoutineId())).append("\"");
198        sb.append(", \"kind\": \"").append(symbol.getRoutineKind()).append("\"");
199        sb.append(", \"name\": \"").append(escapeJson(symbol.getRoutineName())).append("\"");
200        if (symbol.getPackageName() != null) {
201            sb.append(", \"package\": \"").append(escapeJson(symbol.getPackageName())).append("\"");
202        }
203        sb.append(", \"paramCount\": ").append(symbol.getParameters().size());
204        if (includeSourceAnchors) {
205            SourceAnchor anchor = symbol.getDeclarationAnchor();
206            if (anchor != null) {
207                sb.append(", \"sourceAnchor\": ");
208                exportSourceAnchor(sb, anchor);
209            }
210        }
211        sb.append("}");
212    }
213
214    private static void exportRoutineRef(StringBuilder sb, BoundRoutineRef ref, String indent) {
215        sb.append(indent).append("{");
216        sb.append("\"text\": \"").append(escapeJson(ref.getOriginalText())).append("\"");
217        sb.append(", \"status\": \"").append(ref.getBindingStatus()).append("\"");
218        if (ref.getResolvedRoutine() != null) {
219            sb.append(", \"resolvedTo\": \"").append(escapeJson(ref.getResolvedRoutine().getRoutineId())).append("\"");
220        }
221        if (includeEvidence && ref.getEvidence() != null) {
222            sb.append(", \"evidence\": \"").append(escapeJson(ref.getEvidence().message)).append("\"");
223        }
224        if (includeSourceAnchors) {
225            SourceAnchor anchor = ref.getSourceAnchor();
226            if (anchor != null) {
227                sb.append(", \"sourceAnchor\": ");
228                exportSourceAnchor(sb, anchor);
229            }
230        }
231        sb.append("}");
232    }
233
234    private static void exportExternalDependencies(StringBuilder sb, CallGraph callGraph, String indent) {
235        Map<String, List<ExternalCallEdge>> summary = callGraph.getExternalDependencySummary();
236        sb.append(indent).append("\"externalDependencies\": [");
237        boolean first = true;
238        for (Map.Entry<String, List<ExternalCallEdge>> entry : summary.entrySet()) {
239            if (!first) sb.append(",");
240            first = false;
241            sb.append("\n").append(indent).append("  {");
242            List<ExternalCallEdge> edges = entry.getValue();
243            ExternalCallEdge sample = edges.get(0);
244            sb.append("\"name\": \"").append(escapeJson(sample.getExternalName())).append("\"");
245            sb.append(", \"type\": \"").append(escapeJson(sample.getExternalType())).append("\"");
246            if (sample.isSecuritySensitive()) {
247                sb.append(", \"securitySensitive\": true");
248            }
249            sb.append(", \"callCount\": ").append(edges.size());
250            sb.append(", \"callers\": [");
251            boolean firstCaller = true;
252            for (ExternalCallEdge edge : edges) {
253                if (!firstCaller) sb.append(", ");
254                firstCaller = false;
255                sb.append("{\"routineId\": \"").append(escapeJson(edge.getCallerRoutineId())).append("\"");
256                if (includeSourceAnchors && edge.getCallSite() != null) {
257                    sb.append(", \"callSite\": ");
258                    exportSourceAnchor(sb, edge.getCallSite());
259                }
260                sb.append("}");
261            }
262            sb.append("]");
263            sb.append("}");
264        }
265        if (!summary.isEmpty()) {
266            sb.append("\n").append(indent);
267        }
268        sb.append("]\n");
269    }
270
271    private static void exportSourceAnchor(StringBuilder sb, SourceAnchor anchor) {
272        sb.append("{\"startLine\": ").append(anchor.startLine);
273        sb.append(", \"startCol\": ").append(anchor.startCol);
274        sb.append(", \"endLine\": ").append(anchor.endLine);
275        sb.append(", \"endCol\": ").append(anchor.endCol);
276        if (anchor.fileId != null && !anchor.fileId.isEmpty()) {
277            sb.append(", \"fileId\": \"").append(escapeJson(anchor.fileId)).append("\"");
278        }
279        sb.append("}");
280    }
281
282    private static String escapeJson(String s) {
283        if (s == null) return "";
284        return s.replace("\\", "\\\\")
285                .replace("\"", "\\\"")
286                .replace("\n", "\\n")
287                .replace("\r", "\\r")
288                .replace("\t", "\\t");
289    }
290}