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}