001package gudusoft.gsqlparser.analyzer.v2.callgraph; 002 003import gudusoft.gsqlparser.analyzer.v2.ETableAccessKind; 004import gudusoft.gsqlparser.ir.bound.*; 005 006import java.util.*; 007 008/** 009 * Call graph built from a {@link BoundProgram}. 010 * <p> 011 * Nodes represent routines; edges represent calls between routines. 012 * Table accesses are tracked per node. All traversal algorithms use 013 * iterative BFS (no recursion) to avoid stack overflow on deep graphs. 014 */ 015public class CallGraph { 016 017 private final Map<String, CallGraphNode> nodes; 018 private final List<CallEdge> edges; 019 020 // Adjacency lists for efficient queries 021 private final Map<String, List<CallEdge>> outgoing; // caller -> edges 022 private final Map<String, List<CallEdge>> incoming; // callee -> edges 023 024 private CallGraph() { 025 this.nodes = new LinkedHashMap<String, CallGraphNode>(); 026 this.edges = new ArrayList<CallEdge>(); 027 this.outgoing = new HashMap<String, List<CallEdge>>(); 028 this.incoming = new HashMap<String, List<CallEdge>>(); 029 } 030 031 /** 032 * Builds a CallGraph from a BoundProgram. 033 */ 034 public static CallGraph buildFrom(BoundProgram program) { 035 CallGraph graph = new CallGraph(); 036 037 // Create nodes from routineIndex 038 for (Map.Entry<String, BoundRoutineSymbol> entry : program.getRoutineIndex().entrySet()) { 039 BoundRoutineSymbol sym = entry.getValue(); 040 CallGraphNode node = new CallGraphNode( 041 sym.getRoutineId(), 042 sym.getRoutineName(), 043 sym.getPackageName(), 044 sym.getRoutineKind(), 045 sym.getDeclarationAnchor()); 046 graph.nodes.put(sym.getRoutineId(), node); 047 } 048 049 // Create edges from routine refs 050 for (BoundRoutineRef ref : program.getAllRoutineRefs()) { 051 String callerId = ref.getProperty("owningRoutineId"); 052 if (callerId == null) { 053 continue; 054 } 055 056 String calleeId; 057 if (ref.getBindingStatus() == EBindingStatus.EXACT 058 && ref.getResolvedRoutine() != null) { 059 calleeId = ref.getResolvedRoutine().getRoutineId(); 060 } else { 061 // Create an external/unresolved node 062 calleeId = ref.getOriginalText(); 063 if (!graph.nodes.containsKey(calleeId)) { 064 CallGraphNode extNode = new CallGraphNode( 065 calleeId, ref.getOriginalText(), null, 066 ERoutineKind.PROCEDURE, null); 067 graph.nodes.put(calleeId, extNode); 068 } 069 } 070 071 CallEdge edge = new CallEdge( 072 callerId, calleeId, 073 ref.getSourceAnchor(), 074 ref.getEvidence(), 075 ref.getConfidence()); 076 graph.edges.add(edge); 077 078 if (!graph.outgoing.containsKey(callerId)) { 079 graph.outgoing.put(callerId, new ArrayList<CallEdge>()); 080 } 081 graph.outgoing.get(callerId).add(edge); 082 083 if (!graph.incoming.containsKey(calleeId)) { 084 graph.incoming.put(calleeId, new ArrayList<CallEdge>()); 085 } 086 graph.incoming.get(calleeId).add(edge); 087 } 088 089 // Assign table accesses from object refs 090 for (BoundObjectRef objRef : program.getAllObjectRefs()) { 091 String routineId = objRef.getProperty("owningRoutineId"); 092 if (routineId == null) { 093 continue; 094 } 095 CallGraphNode node = graph.nodes.get(routineId); 096 if (node == null) { 097 continue; 098 } 099 100 ETableAccessKind accessKind = TableAccessExtractor.inferAccessKind(objRef); 101 node.addTableAccess(new TableAccess( 102 objRef.getOriginalText(), accessKind, objRef.getSourceAnchor())); 103 } 104 105 return graph; 106 } 107 108 // ---- Query APIs ---- 109 110 public Map<String, CallGraphNode> getNodes() { 111 return Collections.unmodifiableMap(nodes); 112 } 113 114 public List<CallEdge> getEdges() { 115 return Collections.unmodifiableList(edges); 116 } 117 118 public CallGraphNode getNode(String routineId) { 119 return nodes.get(routineId); 120 } 121 122 /** 123 * Returns the direct callees of the given routine. 124 */ 125 public List<String> getDirectCallees(String routineId) { 126 List<CallEdge> out = outgoing.get(routineId); 127 if (out == null) { 128 return Collections.emptyList(); 129 } 130 List<String> result = new ArrayList<String>(); 131 for (CallEdge edge : out) { 132 if (!result.contains(edge.getCalleeRoutineId())) { 133 result.add(edge.getCalleeRoutineId()); 134 } 135 } 136 return result; 137 } 138 139 /** 140 * Returns the direct callers of the given routine. 141 */ 142 public List<String> getDirectCallers(String routineId) { 143 List<CallEdge> in = incoming.get(routineId); 144 if (in == null) { 145 return Collections.emptyList(); 146 } 147 List<String> result = new ArrayList<String>(); 148 for (CallEdge edge : in) { 149 if (!result.contains(edge.getCallerRoutineId())) { 150 result.add(edge.getCallerRoutineId()); 151 } 152 } 153 return result; 154 } 155 156 /** 157 * Returns all transitively reachable callees using iterative BFS. 158 */ 159 public Set<String> getTransitiveCallees(String routineId, int maxDepth) { 160 Set<String> visited = new LinkedHashSet<String>(); 161 Deque<String> queue = new ArrayDeque<String>(); 162 Map<String, Integer> depthMap = new HashMap<String, Integer>(); 163 164 queue.add(routineId); 165 depthMap.put(routineId, 0); 166 167 while (!queue.isEmpty()) { 168 String current = queue.poll(); 169 int currentDepth = depthMap.get(current); 170 171 if (currentDepth >= maxDepth) { 172 continue; 173 } 174 175 for (String callee : getDirectCallees(current)) { 176 if (!visited.contains(callee) && !callee.equals(routineId)) { 177 visited.add(callee); 178 if (!depthMap.containsKey(callee)) { 179 depthMap.put(callee, currentDepth + 1); 180 queue.add(callee); 181 } 182 } 183 } 184 } 185 return visited; 186 } 187 188 /** 189 * Returns all transitive callers using iterative BFS. 190 */ 191 public Set<String> getTransitiveCallers(String routineId, int maxDepth) { 192 Set<String> visited = new LinkedHashSet<String>(); 193 Deque<String> queue = new ArrayDeque<String>(); 194 Map<String, Integer> depthMap = new HashMap<String, Integer>(); 195 196 queue.add(routineId); 197 depthMap.put(routineId, 0); 198 199 while (!queue.isEmpty()) { 200 String current = queue.poll(); 201 int currentDepth = depthMap.get(current); 202 203 if (currentDepth >= maxDepth) { 204 continue; 205 } 206 207 for (String caller : getDirectCallers(current)) { 208 if (!visited.contains(caller) && !caller.equals(routineId)) { 209 visited.add(caller); 210 if (!depthMap.containsKey(caller)) { 211 depthMap.put(caller, currentDepth + 1); 212 queue.add(caller); 213 } 214 } 215 } 216 } 217 return visited; 218 } 219 220 /** 221 * Finds all nodes that access a given table. 222 */ 223 public List<CallGraphNode> getNodesAccessingTable(String tableName, ETableAccessKind filter) { 224 String normalizedSearch = TableAccessExtractor.normalizeTableName(tableName); 225 List<CallGraphNode> result = new ArrayList<CallGraphNode>(); 226 for (CallGraphNode node : nodes.values()) { 227 for (TableAccess ta : node.getTableAccesses()) { 228 String normalizedTable = TableAccessExtractor.normalizeTableName(ta.getTableName()); 229 if (normalizedTable.equals(normalizedSearch)) { 230 if (filter == null || ta.getAccessKind() == filter 231 || ta.getAccessKind() == ETableAccessKind.READ_WRITE) { 232 result.add(node); 233 break; 234 } 235 } 236 } 237 } 238 return result; 239 } 240 241}