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}