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