001package gudusoft.gsqlparser.ir.builder.oracle;
002
003import gudusoft.gsqlparser.EExpressionType;
004import gudusoft.gsqlparser.TBaseType;
005import gudusoft.gsqlparser.TCustomSqlStatement;
006import gudusoft.gsqlparser.TStatementList;
007import gudusoft.gsqlparser.analyzer.v2.AnalyzerV2Config;
008import gudusoft.gsqlparser.ir.bound.*;
009import gudusoft.gsqlparser.ir.common.Confidence;
010import gudusoft.gsqlparser.ir.common.Evidence;
011import gudusoft.gsqlparser.ir.common.EvidenceKind;
012import gudusoft.gsqlparser.ir.common.SourceAnchor;
013import gudusoft.gsqlparser.ir.builder.IBoundIRBuilder;
014import gudusoft.gsqlparser.nodes.TExpression;
015import gudusoft.gsqlparser.nodes.TObjectName;
016import gudusoft.gsqlparser.nodes.TParseTreeVisitor;
017import gudusoft.gsqlparser.stmt.TAssignStmt;
018import gudusoft.gsqlparser.stmt.TCommonBlock;
019import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction;
020import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage;
021import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure;
022
023import java.util.*;
024
025/**
026 * Oracle PL/SQL implementation of {@link IBoundIRBuilder}.
027 * <p>
028 * Orchestrates three internal phases:
029 * <ol>
030 *   <li>Create global scope, iterate statements</li>
031 *   <li>Run {@link PlsqlSymbolCollector} (visitor) to extract routines, variables, cursors, calls</li>
032 *   <li>Run {@link RoutineRefResolver} to match {@code BoundRoutineRef} to {@code BoundRoutineSymbol}</li>
033 * </ol>
034 * <p>
035 * Leverages TSQLResolver2's already-populated results (auto-invoked by {@code parse()})
036 * for table/column resolution, and supplements with PL/SQL-specific symbol extraction.
037 */
038public class OracleBoundIRBuilder implements IBoundIRBuilder {
039
040    @Override
041    public BoundProgram build(TStatementList stmts, AnalyzerV2Config config) {
042        BoundProgram program = new BoundProgram();
043
044        // Phase 1: Create global scope
045        BoundScope globalScope = new BoundScope(EScopeKind.GLOBAL, null, null);
046        program.addScope(globalScope);
047
048        if (stmts == null || stmts.size() == 0) {
049            return program;
050        }
051
052        // Phase 2: Walk AST with PlsqlSymbolCollector
053        PlsqlSymbolCollector collector = new PlsqlSymbolCollector(program, globalScope, config);
054        for (int i = 0; i < stmts.size(); i++) {
055            TCustomSqlStatement stmt = stmts.get(i);
056            if (stmt != null) {
057                stmt.acceptChildren(collector);
058            }
059        }
060
061        // Phase 2b: Merge forward declarations with implementations.
062        // In PL/SQL package bodies, a forward declaration (no body) followed by its
063        // implementation produces two routine entries differing only in name case.
064        // Keep only the implementation.
065        mergeForwardDeclarations(program);
066
067        // Phase 2c: Detect parameterless function calls in assignment RHS.
068        // PL/SQL allows calling zero-param functions without parentheses (v := func_name;).
069        // These appear as simple_object_name_t expressions, not TFunctionCall nodes.
070        // We need the complete routine table (after Phase 2b) to distinguish them from variables.
071        detectParameterlessFunctionCalls(program, stmts);
072
073        // Phase 3: Resolve routine references
074        RoutineRefResolver.resolve(program);
075
076        return program;
077    }
078
079    /**
080     * Walks the AST a second time to find assignments where the RHS is a bare identifier
081     * that matches a declared function name. Creates BoundRoutineRef entries for these.
082     */
083    private void detectParameterlessFunctionCalls(BoundProgram program, TStatementList stmts) {
084        // Build set of function names (case-insensitive)
085        Set<String> functionNames = new HashSet<String>();
086        for (BoundRoutineSymbol routine : program.getRoutineIndex().values()) {
087            ERoutineKind kind = routine.getRoutineKind();
088            if (kind == ERoutineKind.FUNCTION || kind == ERoutineKind.NESTED_FUNCTION) {
089                functionNames.add(routine.getRoutineName().toUpperCase());
090            }
091        }
092        if (functionNames.isEmpty()) {
093            return;
094        }
095
096        // Collect existing routine ref names to avoid duplicates
097        Set<String> existingRefKeys = new HashSet<String>();
098        for (BoundRoutineRef ref : program.getAllRoutineRefs()) {
099            String owning = ref.getProperty("owningRoutineId");
100            existingRefKeys.add((owning != null ? owning : "") + "|" + ref.getOriginalText().toUpperCase());
101        }
102
103        ParameterlessFuncDetector detector = new ParameterlessFuncDetector(
104                program, functionNames, existingRefKeys);
105        for (int i = 0; i < stmts.size(); i++) {
106            TCustomSqlStatement stmt = stmts.get(i);
107            if (stmt != null) {
108                stmt.acceptChildren(detector);
109            }
110        }
111    }
112
113    /**
114     * Scans the routine index for case-insensitive duplicates within the same
115     * package and parameter count (forward declaration vs. implementation).
116     * Removes the earlier entry (forward declaration) and keeps the later one (implementation).
117     */
118    private void mergeForwardDeclarations(BoundProgram program) {
119        Map<String, BoundRoutineSymbol> routineIndex = program.getRoutineIndex();
120        if (routineIndex.size() <= 1) {
121            return;
122        }
123
124        // Group by case-insensitive routineId
125        Map<String, List<String>> ciGroups = new LinkedHashMap<String, List<String>>();
126        for (String routineId : routineIndex.keySet()) {
127            String ciKey = routineId.toUpperCase();
128            List<String> group = ciGroups.get(ciKey);
129            if (group == null) {
130                group = new ArrayList<String>();
131                ciGroups.put(ciKey, group);
132            }
133            group.add(routineId);
134        }
135
136        // For groups with duplicates, keep the last entry (implementation) and remove others
137        for (List<String> group : ciGroups.values()) {
138            if (group.size() > 1) {
139                // Keep the last entry (implementation comes after forward declaration in source)
140                for (int i = 0; i < group.size() - 1; i++) {
141                    program.unregisterRoutine(group.get(i));
142                }
143            }
144        }
145    }
146
147    /**
148     * Visitor that tracks routine context and detects parameterless function
149     * calls across all expression contexts (assignments, IF conditions, SQL, etc.).
150     * <p>
151     * PL/SQL allows calling zero-param functions without parentheses. These appear
152     * as simple_object_name_t or object_access_t expressions rather than TFunctionCall
153     * nodes. This detector matches such expressions against the collected function
154     * name set to identify them as calls.
155     */
156    private static class ParameterlessFuncDetector extends TParseTreeVisitor {
157
158        private final BoundProgram program;
159        private final Set<String> functionNames;
160        private final Set<String> existingRefKeys;
161        private final Deque<String> routineIdStack = new ArrayDeque<String>();
162        private String currentRoutineId;
163        private String currentPackageName;
164        /** Tracks whether each TCommonBlock preVisit pushed a routine. */
165        private final Deque<Boolean> commonBlockPushedRoutine = new ArrayDeque<Boolean>();
166
167        /** Known Oracle pseudo-columns to skip. */
168        private static boolean isOraclePseudoColumn(String name) {
169            switch (name.toUpperCase()) {
170                case "SYSDATE": case "SYSTIMESTAMP":
171                case "CURRENT_DATE": case "CURRENT_TIMESTAMP": case "LOCALTIMESTAMP":
172                case "USER": case "UID":
173                case "ROWNUM": case "ROWID":
174                case "LEVEL":
175                case "SQLCODE": case "SQLERRM":
176                case "TRUE": case "FALSE":
177                case "NULL":
178                    return true;
179                default:
180                    return false;
181            }
182        }
183
184        ParameterlessFuncDetector(BoundProgram program, Set<String> functionNames,
185                                   Set<String> existingRefKeys) {
186            this.program = program;
187            this.functionNames = functionNames;
188            this.existingRefKeys = existingRefKeys;
189        }
190
191        // ---- Package tracking ----
192
193        @Override
194        public void preVisit(TPlsqlCreatePackage node) {
195            TObjectName pkgName = node.getPackageName();
196            String name = pkgName != null ? pkgName.toString().trim() : "";
197            int dot = name.lastIndexOf('.');
198            if (dot >= 0) {
199                name = name.substring(dot + 1);
200            }
201            currentPackageName = name;
202        }
203
204        @Override
205        public void postVisit(TPlsqlCreatePackage node) {
206            currentPackageName = null;
207        }
208
209        // ---- Routine tracking ----
210
211        @Override
212        public void preVisit(TPlsqlCreateProcedure node) {
213            String name = node.getProcedureName() != null
214                    ? node.getProcedureName().toString().trim() : "";
215            int dot = name.lastIndexOf('.');
216            if (dot >= 0) {
217                name = name.substring(dot + 1);
218            }
219            boolean isNested = node.getKind() != TBaseType.kind_create;
220            String kind = isNested ? "NP" : "P";
221            int paramCount = node.getParameterDeclarations() != null
222                    ? node.getParameterDeclarations().size() : 0;
223            pushRoutineId(buildRoutineId(name, kind, paramCount));
224        }
225
226        @Override
227        public void postVisit(TPlsqlCreateProcedure node) {
228            popRoutineId();
229        }
230
231        @Override
232        public void preVisit(TPlsqlCreateFunction node) {
233            String name = node.getFunctionName() != null
234                    ? node.getFunctionName().toString().trim() : "";
235            int dot = name.lastIndexOf('.');
236            if (dot >= 0) {
237                name = name.substring(dot + 1);
238            }
239            boolean isNested = node.getKind() != TBaseType.kind_create;
240            String kind = isNested ? "NF" : "F";
241            int paramCount = node.getParameterDeclarations() != null
242                    ? node.getParameterDeclarations().size() : 0;
243            pushRoutineId(buildRoutineId(name, kind, paramCount));
244        }
245
246        @Override
247        public void postVisit(TPlsqlCreateFunction node) {
248            popRoutineId();
249        }
250
251        @Override
252        public void preVisit(TCommonBlock node) {
253            // Anonymous blocks and nested routines
254            if (node.getDeclareStatements() != null || node.getBodyStatements() != null) {
255                if (currentRoutineId == null) {
256                    pushRoutineId("<anonymous>/A(0)");
257                    commonBlockPushedRoutine.push(true);
258                } else {
259                    commonBlockPushedRoutine.push(false);
260                }
261            } else {
262                commonBlockPushedRoutine.push(false);
263            }
264        }
265
266        @Override
267        public void postVisit(TCommonBlock node) {
268            if (!commonBlockPushedRoutine.isEmpty() && commonBlockPushedRoutine.pop()) {
269                popRoutineId();
270            }
271        }
272
273        // ---- Expression-level detection ----
274
275        @Override
276        public void preVisit(TExpression node) {
277            if (currentRoutineId == null) {
278                return;
279            }
280            EExpressionType exprType = node.getExpressionType();
281            if (exprType == EExpressionType.simple_object_name_t) {
282                checkBareIdentifier(node);
283            } else if (exprType == EExpressionType.object_access_t) {
284                checkQualifiedIdentifier(node);
285            }
286        }
287
288        /**
289         * Checks an identifier (simple_object_name_t) against known function names.
290         * Handles both bare names ({@code get_value}) and dotted names ({@code pkg.get_value}).
291         * For dotted names, checks the terminal part against the function name set.
292         */
293        private void checkBareIdentifier(TExpression expr) {
294            String name = expr.toString().trim();
295            if (name.isEmpty()) {
296                return;
297            }
298            int dot = name.lastIndexOf('.');
299            if (dot >= 0) {
300                // Dotted name (e.g., pkg.func) — check terminal part
301                String terminal = name.substring(dot + 1).trim();
302                if (terminal.isEmpty() || isOraclePseudoColumn(terminal)) {
303                    return;
304                }
305                if (!functionNames.contains(terminal.toUpperCase())) {
306                    return;
307                }
308            } else {
309                // Bare name — check directly
310                if (isOraclePseudoColumn(name)) {
311                    return;
312                }
313                if (!functionNames.contains(name.toUpperCase())) {
314                    return;
315                }
316            }
317            addRefIfNew(name, PlsqlSymbolCollector.splitName(name), expr);
318        }
319
320        /**
321         * Checks a qualified identifier (object_access_t like pkg.func) where the
322         * terminal part matches a known function name. The full qualified name is
323         * used as the ref text so the resolver can match it.
324         */
325        private void checkQualifiedIdentifier(TExpression expr) {
326            String qualName = expr.toString().trim();
327            if (qualName.isEmpty()) {
328                return;
329            }
330            int dot = qualName.lastIndexOf('.');
331            if (dot < 0) {
332                return;
333            }
334            String terminal = qualName.substring(dot + 1).trim();
335            if (terminal.isEmpty() || isOraclePseudoColumn(terminal)) {
336                return;
337            }
338            if (!functionNames.contains(terminal.toUpperCase())) {
339                return;
340            }
341            addRefIfNew(qualName, PlsqlSymbolCollector.splitName(qualName), expr);
342        }
343
344        private void addRefIfNew(String name, List<String> nameParts, TExpression expr) {
345            String refKey = (currentRoutineId != null ? currentRoutineId : "")
346                    + "|" + name.toUpperCase();
347            if (existingRefKeys.contains(refKey)) {
348                return;
349            }
350            existingRefKeys.add(refKey);
351
352            BoundRoutineRef ref = new BoundRoutineRef(
353                    name, nameParts,
354                    EBindingStatus.UNRESOLVED_SOFT, null,
355                    null, Collections.<BoundArgument>emptyList(),
356                    new Evidence(EvidenceKind.STATIC_RESOLVED,
357                            "Parameterless function call"),
358                    Confidence.HIGH);
359            ref.setSourceAnchor(SourceAnchor.from(expr));
360            if (currentRoutineId != null) {
361                ref.setProperty("owningRoutineId", currentRoutineId);
362            }
363            program.addRoutineRef(ref);
364        }
365
366        // ---- Helpers ----
367
368        private String buildRoutineId(String name, String kindCode, int paramCount) {
369            StringBuilder sb = new StringBuilder();
370            if (currentPackageName != null && !currentPackageName.isEmpty()) {
371                sb.append(currentPackageName).append('.');
372            }
373            sb.append(name).append('/').append(kindCode).append('(').append(paramCount).append(')');
374            return sb.toString();
375        }
376
377        private void pushRoutineId(String routineId) {
378            routineIdStack.push(routineId);
379            currentRoutineId = routineId;
380        }
381
382        private void popRoutineId() {
383            if (!routineIdStack.isEmpty()) {
384                routineIdStack.pop();
385            }
386            currentRoutineId = routineIdStack.isEmpty() ? null : routineIdStack.peek();
387        }
388    }
389}