001package gudusoft.gsqlparser.analyzer.v2.impact; 002 003import gudusoft.gsqlparser.analyzer.v2.ETableAccessKind; 004import gudusoft.gsqlparser.analyzer.v2.ImpactEntry; 005import gudusoft.gsqlparser.analyzer.v2.ImpactResult; 006import gudusoft.gsqlparser.analyzer.v2.callgraph.CallEdge; 007import gudusoft.gsqlparser.analyzer.v2.callgraph.CallGraph; 008import gudusoft.gsqlparser.analyzer.v2.callgraph.CallGraphNode; 009import gudusoft.gsqlparser.analyzer.v2.callgraph.TableAccess; 010import gudusoft.gsqlparser.ir.common.SourceAnchor; 011 012import java.util.*; 013 014/** 015 * Impact analysis engine using real BFS traversal with path reconstruction. 016 * <p> 017 * Provides accurate graph distances (not sequential counters) and 018 * reconstructed shortest paths from source to each impacted entity. 019 */ 020public class ImpactEngine { 021 022 private ImpactEngine() {} 023 024 /** 025 * Computes routine impact: finds all routines and tables transitively 026 * affected by a change to the given routine, using forward BFS. 027 * 028 * @param callGraph the call graph to traverse 029 * @param routineId the source routine to analyze impact from 030 * @param maxDepth maximum BFS depth (from config) 031 * @return impact result with real graph distances and reconstructed paths 032 */ 033 public static ImpactResult getRoutineImpact(CallGraph callGraph, String routineId, int maxDepth) { 034 if (callGraph == null) { 035 return emptyResult(); 036 } 037 038 // BFS forward from source 039 Map<String, Integer> distance = new LinkedHashMap<String, Integer>(); 040 Map<String, String> predecessor = new LinkedHashMap<String, String>(); 041 Deque<String> queue = new ArrayDeque<String>(); 042 043 distance.put(routineId, 0); 044 queue.add(routineId); 045 046 while (!queue.isEmpty()) { 047 String current = queue.poll(); 048 int currentDepth = distance.get(current); 049 050 if (currentDepth >= maxDepth) { 051 continue; 052 } 053 054 for (String callee : callGraph.getDirectCallees(current)) { 055 if (!distance.containsKey(callee)) { 056 distance.put(callee, currentDepth + 1); 057 predecessor.put(callee, current); 058 queue.add(callee); 059 } 060 } 061 } 062 063 // Build results 064 List<ImpactEntry> impactedRoutines = new ArrayList<ImpactEntry>(); 065 List<ImpactEntry> impactedTables = new ArrayList<ImpactEntry>(); 066 067 // Include the source routine's own table accesses at depth 0 068 collectTableImpact(callGraph, routineId, 0, 069 Collections.singletonList(routineId), impactedTables); 070 071 for (Map.Entry<String, Integer> entry : distance.entrySet()) { 072 String nodeId = entry.getKey(); 073 if (nodeId.equals(routineId)) { 074 continue; // skip source 075 } 076 int depth = entry.getValue(); 077 List<String> path = reconstructPath(predecessor, routineId, nodeId); 078 079 CallGraphNode node = callGraph.getNode(nodeId); 080 String displayName = node != null ? node.getRoutineName() : nodeId; 081 SourceAnchor anchor = node != null ? node.getDeclarationAnchor() : null; 082 083 impactedRoutines.add(new ImpactEntry( 084 nodeId, displayName, depth, path, anchor)); 085 086 // Collect table accesses for this callee 087 collectTableImpact(callGraph, nodeId, depth, path, impactedTables); 088 } 089 090 return new ImpactResult(impactedRoutines, impactedTables); 091 } 092 093 /** 094 * Computes table impact: finds all routines affected by a change to the given table. 095 * Uses reverse BFS from each direct accessor to find transitive callers. 096 * 097 * @param callGraph the call graph 098 * @param tableName the table name to analyze 099 * @param accessFilter optional filter (null = all access types) 100 * @param maxDepth maximum BFS depth 101 * @return impact result 102 */ 103 public static ImpactResult getTableImpact(CallGraph callGraph, String tableName, 104 ETableAccessKind accessFilter, int maxDepth) { 105 if (callGraph == null) { 106 return emptyResult(); 107 } 108 109 List<CallGraphNode> directAccessors = 110 callGraph.getNodesAccessingTable(tableName, accessFilter); 111 112 List<ImpactEntry> impactedRoutines = new ArrayList<ImpactEntry>(); 113 List<ImpactEntry> impactedTables = new ArrayList<ImpactEntry>(); 114 115 // Add the table itself at depth 0 116 impactedTables.add(new ImpactEntry( 117 tableName, tableName, 0, 118 Collections.singletonList(tableName), null)); 119 120 Set<String> seen = new LinkedHashSet<String>(); 121 122 for (CallGraphNode accessor : directAccessors) { 123 String accessorId = accessor.getRoutineId(); 124 if (!seen.add(accessorId)) { 125 continue; 126 } 127 128 // Direct accessor at depth 1 129 List<String> directPath = new ArrayList<String>(); 130 directPath.add(tableName); 131 directPath.add(accessorId); 132 133 impactedRoutines.add(new ImpactEntry( 134 accessorId, accessor.getRoutineName(), 1, 135 directPath, accessor.getDeclarationAnchor())); 136 137 // Reverse BFS from accessor to find transitive callers 138 Map<String, Integer> callerDistance = new LinkedHashMap<String, Integer>(); 139 Map<String, String> callerPredecessor = new LinkedHashMap<String, String>(); 140 Deque<String> queue = new ArrayDeque<String>(); 141 142 callerDistance.put(accessorId, 0); 143 queue.add(accessorId); 144 145 while (!queue.isEmpty()) { 146 String current = queue.poll(); 147 int currentDepth = callerDistance.get(current); 148 149 if (currentDepth >= maxDepth - 1) { // -1 because accessor is already depth 1 150 continue; 151 } 152 153 for (String caller : callGraph.getDirectCallers(current)) { 154 if (!callerDistance.containsKey(caller) && seen.add(caller)) { 155 callerDistance.put(caller, currentDepth + 1); 156 callerPredecessor.put(caller, current); 157 queue.add(caller); 158 159 int totalDepth = currentDepth + 2; // +2 = table→accessor + caller chain 160 List<String> callerPath = new ArrayList<String>(); 161 callerPath.add(tableName); 162 // Reconstruct accessor → ... → caller 163 List<String> chainPath = reconstructPath( 164 callerPredecessor, accessorId, caller); 165 callerPath.addAll(chainPath); 166 167 CallGraphNode callerNode = callGraph.getNode(caller); 168 String name = callerNode != null ? callerNode.getRoutineName() : caller; 169 SourceAnchor callerAnchor = callerNode != null 170 ? callerNode.getDeclarationAnchor() : null; 171 172 impactedRoutines.add(new ImpactEntry( 173 caller, name, totalDepth, callerPath, callerAnchor)); 174 } 175 } 176 } 177 } 178 179 return new ImpactResult(impactedRoutines, impactedTables); 180 } 181 182 /** 183 * Reconstructs the shortest path from source to target using the predecessor map. 184 */ 185 private static List<String> reconstructPath(Map<String, String> predecessor, 186 String source, String target) { 187 List<String> path = new ArrayList<String>(); 188 String current = target; 189 while (current != null && !current.equals(source)) { 190 path.add(current); 191 current = predecessor.get(current); 192 } 193 path.add(source); 194 Collections.reverse(path); 195 return path; 196 } 197 198 private static void collectTableImpact(CallGraph callGraph, String routineId, 199 int depth, List<String> basePath, 200 List<ImpactEntry> impactedTables) { 201 CallGraphNode node = callGraph.getNode(routineId); 202 if (node == null) { 203 return; 204 } 205 for (TableAccess ta : node.getTableAccesses()) { 206 List<String> tablePath = new ArrayList<String>(basePath); 207 tablePath.add(ta.getTableName()); 208 impactedTables.add(new ImpactEntry( 209 ta.getTableName(), ta.getTableName(), depth, 210 tablePath, ta.getAnchor())); 211 } 212 } 213 214 private static ImpactResult emptyResult() { 215 return new ImpactResult( 216 Collections.<ImpactEntry>emptyList(), 217 Collections.<ImpactEntry>emptyList()); 218 } 219}