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}