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    private final List<ExternalCallEdge> externalEdges;
020
021    // Adjacency lists for efficient queries
022    private final Map<String, List<CallEdge>> outgoing; // caller -> edges
023    private final Map<String, List<CallEdge>> incoming; // callee -> edges
024
025    private CallGraph() {
026        this.nodes = new LinkedHashMap<String, CallGraphNode>();
027        this.edges = new ArrayList<CallEdge>();
028        this.externalEdges = new ArrayList<ExternalCallEdge>();
029        this.outgoing = new HashMap<String, List<CallEdge>>();
030        this.incoming = new HashMap<String, List<CallEdge>>();
031    }
032
033    /**
034     * Builds a CallGraph from a BoundProgram.
035     */
036    public static CallGraph buildFrom(BoundProgram program) {
037        CallGraph graph = new CallGraph();
038
039        // Create nodes from routineIndex
040        for (Map.Entry<String, BoundRoutineSymbol> entry : program.getRoutineIndex().entrySet()) {
041            BoundRoutineSymbol sym = entry.getValue();
042            CallGraphNode node = new CallGraphNode(
043                    sym.getRoutineId(),
044                    sym.getRoutineName(),
045                    sym.getPackageName(),
046                    sym.getRoutineKind(),
047                    sym.getDeclarationAnchor());
048            graph.nodes.put(sym.getRoutineId(), node);
049        }
050
051        // Create edges from routine refs
052        for (BoundRoutineRef ref : program.getAllRoutineRefs()) {
053            String callerId = ref.getProperty("owningRoutineId");
054            if (callerId == null) {
055                continue;
056            }
057
058            // External dependency: create ExternalCallEdge, not a node
059            if (Boolean.TRUE.equals(ref.getProperty("externalDependency"))) {
060                String extName = ref.getProperty("externalName");
061                if (extName == null) extName = ref.getOriginalText();
062                String extType = ref.getProperty("externalType");
063                if (extType == null) extType = "BUILTIN_FUNCTION";
064                boolean secSensitive = Boolean.TRUE.equals(ref.getProperty("securitySensitive"));
065                graph.externalEdges.add(new ExternalCallEdge(
066                        callerId, extName, extType,
067                        ref.getSourceAnchor(), secSensitive));
068                continue;
069            }
070
071            String calleeId;
072            if (ref.getBindingStatus() == EBindingStatus.EXACT
073                    && ref.getResolvedRoutine() != null) {
074                calleeId = ref.getResolvedRoutine().getRoutineId();
075            } else {
076                // Create an external/unresolved node
077                calleeId = ref.getOriginalText();
078                if (!graph.nodes.containsKey(calleeId)) {
079                    CallGraphNode extNode = new CallGraphNode(
080                            calleeId, ref.getOriginalText(), null,
081                            ERoutineKind.PROCEDURE, null);
082                    graph.nodes.put(calleeId, extNode);
083                }
084            }
085
086            CallEdge edge = new CallEdge(
087                    callerId, calleeId,
088                    ref.getSourceAnchor(),
089                    ref.getEvidence(),
090                    ref.getConfidence());
091            graph.edges.add(edge);
092
093            if (!graph.outgoing.containsKey(callerId)) {
094                graph.outgoing.put(callerId, new ArrayList<CallEdge>());
095            }
096            graph.outgoing.get(callerId).add(edge);
097
098            if (!graph.incoming.containsKey(calleeId)) {
099                graph.incoming.put(calleeId, new ArrayList<CallEdge>());
100            }
101            graph.incoming.get(calleeId).add(edge);
102        }
103
104        // Assign table accesses from object refs
105        for (BoundObjectRef objRef : program.getAllObjectRefs()) {
106            String routineId = objRef.getProperty("owningRoutineId");
107            if (routineId == null) {
108                continue;
109            }
110            CallGraphNode node = graph.nodes.get(routineId);
111            if (node == null) {
112                continue;
113            }
114
115            ETableAccessKind accessKind = TableAccessExtractor.inferAccessKind(objRef);
116            node.addTableAccess(new TableAccess(
117                    objRef.getOriginalText(), accessKind, objRef.getSourceAnchor()));
118        }
119
120        return graph;
121    }
122
123    // ---- Query APIs ----
124
125    public Map<String, CallGraphNode> getNodes() {
126        return Collections.unmodifiableMap(nodes);
127    }
128
129    public List<CallEdge> getEdges() {
130        return Collections.unmodifiableList(edges);
131    }
132
133    public CallGraphNode getNode(String routineId) {
134        return nodes.get(routineId);
135    }
136
137    /**
138     * Returns the direct callees of the given routine.
139     */
140    public List<String> getDirectCallees(String routineId) {
141        List<CallEdge> out = outgoing.get(routineId);
142        if (out == null) {
143            return Collections.emptyList();
144        }
145        List<String> result = new ArrayList<String>();
146        for (CallEdge edge : out) {
147            if (!result.contains(edge.getCalleeRoutineId())) {
148                result.add(edge.getCalleeRoutineId());
149            }
150        }
151        return result;
152    }
153
154    /**
155     * Returns the direct callers of the given routine.
156     */
157    public List<String> getDirectCallers(String routineId) {
158        List<CallEdge> in = incoming.get(routineId);
159        if (in == null) {
160            return Collections.emptyList();
161        }
162        List<String> result = new ArrayList<String>();
163        for (CallEdge edge : in) {
164            if (!result.contains(edge.getCallerRoutineId())) {
165                result.add(edge.getCallerRoutineId());
166            }
167        }
168        return result;
169    }
170
171    /**
172     * Returns all transitively reachable callees using iterative BFS.
173     */
174    public Set<String> getTransitiveCallees(String routineId, int maxDepth) {
175        Set<String> visited = new LinkedHashSet<String>();
176        Deque<String> queue = new ArrayDeque<String>();
177        Map<String, Integer> depthMap = new HashMap<String, Integer>();
178
179        queue.add(routineId);
180        depthMap.put(routineId, 0);
181
182        while (!queue.isEmpty()) {
183            String current = queue.poll();
184            int currentDepth = depthMap.get(current);
185
186            if (currentDepth >= maxDepth) {
187                continue;
188            }
189
190            for (String callee : getDirectCallees(current)) {
191                if (!visited.contains(callee) && !callee.equals(routineId)) {
192                    visited.add(callee);
193                    if (!depthMap.containsKey(callee)) {
194                        depthMap.put(callee, currentDepth + 1);
195                        queue.add(callee);
196                    }
197                }
198            }
199        }
200        return visited;
201    }
202
203    /**
204     * Returns all transitive callers using iterative BFS.
205     */
206    public Set<String> getTransitiveCallers(String routineId, int maxDepth) {
207        Set<String> visited = new LinkedHashSet<String>();
208        Deque<String> queue = new ArrayDeque<String>();
209        Map<String, Integer> depthMap = new HashMap<String, Integer>();
210
211        queue.add(routineId);
212        depthMap.put(routineId, 0);
213
214        while (!queue.isEmpty()) {
215            String current = queue.poll();
216            int currentDepth = depthMap.get(current);
217
218            if (currentDepth >= maxDepth) {
219                continue;
220            }
221
222            for (String caller : getDirectCallers(current)) {
223                if (!visited.contains(caller) && !caller.equals(routineId)) {
224                    visited.add(caller);
225                    if (!depthMap.containsKey(caller)) {
226                        depthMap.put(caller, currentDepth + 1);
227                        queue.add(caller);
228                    }
229                }
230            }
231        }
232        return visited;
233    }
234
235    /**
236     * Finds all nodes that access a given table.
237     */
238    public List<CallGraphNode> getNodesAccessingTable(String tableName, ETableAccessKind filter) {
239        String normalizedSearch = TableAccessExtractor.normalizeTableName(tableName);
240        List<CallGraphNode> result = new ArrayList<CallGraphNode>();
241        for (CallGraphNode node : nodes.values()) {
242            for (TableAccess ta : node.getTableAccesses()) {
243                String normalizedTable = TableAccessExtractor.normalizeTableName(ta.getTableName());
244                if (normalizedTable.equals(normalizedSearch)) {
245                    if (filter == null || ta.getAccessKind() == filter
246                            || ta.getAccessKind() == ETableAccessKind.READ_WRITE) {
247                        result.add(node);
248                        break;
249                    }
250                }
251            }
252        }
253        return result;
254    }
255
256    // ---- External dependency APIs ----
257
258    /**
259     * Returns all external dependency edges.
260     */
261    public List<ExternalCallEdge> getExternalEdges() {
262        return Collections.unmodifiableList(externalEdges);
263    }
264
265    /**
266     * Returns external callees for a given routine.
267     */
268    public List<ExternalCallEdge> getExternalCallees(String routineId) {
269        List<ExternalCallEdge> result = new ArrayList<ExternalCallEdge>();
270        for (ExternalCallEdge edge : externalEdges) {
271            if (edge.getCallerRoutineId().equals(routineId)) {
272                result.add(edge);
273            }
274        }
275        return result;
276    }
277
278    /**
279     * Returns external callers of a given external name.
280     */
281    public List<ExternalCallEdge> getExternalCallers(String externalName) {
282        String upper = externalName.toUpperCase();
283        List<ExternalCallEdge> result = new ArrayList<ExternalCallEdge>();
284        for (ExternalCallEdge edge : externalEdges) {
285            if (edge.getExternalName().toUpperCase().equals(upper)) {
286                result.add(edge);
287            }
288        }
289        return result;
290    }
291
292    /**
293     * Returns a summary of external dependencies grouped by external name.
294     */
295    public Map<String, List<ExternalCallEdge>> getExternalDependencySummary() {
296        Map<String, List<ExternalCallEdge>> summary = new LinkedHashMap<String, List<ExternalCallEdge>>();
297        for (ExternalCallEdge edge : externalEdges) {
298            String key = edge.getExternalName().toUpperCase();
299            List<ExternalCallEdge> list = summary.get(key);
300            if (list == null) {
301                list = new ArrayList<ExternalCallEdge>();
302                summary.put(key, list);
303            }
304            list.add(edge);
305        }
306        return summary;
307    }
308
309}