001package gudusoft.gsqlparser.ir.builder.mssql; 002 003import gudusoft.gsqlparser.*; 004import gudusoft.gsqlparser.analyzer.v2.AnalyzerV2Config; 005import gudusoft.gsqlparser.ir.bound.*; 006import gudusoft.gsqlparser.ir.common.*; 007import gudusoft.gsqlparser.nodes.*; 008import gudusoft.gsqlparser.stmt.*; 009import gudusoft.gsqlparser.stmt.mssql.*; 010 011import java.util.*; 012 013/** 014 * T-SQL AST visitor that collects routine declarations, call references, 015 * variable declarations, and table references from SQL Server scripts. 016 * <p> 017 * Extends {@link TParseTreeVisitor} and uses preVisit/postVisit pairs 018 * for scope-creating nodes and preVisit only for leaf collection. 019 */ 020public class TsqlSymbolCollector extends TParseTreeVisitor { 021 022 /** T-SQL XML data type methods and .WRITE() column update method. 023 * When these appear as something.method(), they are NOT user-defined function calls. */ 024 private static final Set<String> DOT_METHOD_NAMES = new HashSet<String>(); 025 static { 026 // XML data type methods 027 DOT_METHOD_NAMES.add("NODES"); 028 DOT_METHOD_NAMES.add("VALUE"); 029 DOT_METHOD_NAMES.add("QUERY"); 030 DOT_METHOD_NAMES.add("EXIST"); 031 DOT_METHOD_NAMES.add("MODIFY"); 032 // .WRITE() partial column update 033 DOT_METHOD_NAMES.add("WRITE"); 034 } 035 036 private final BoundProgram program; 037 private final Deque<BoundScope> scopeStack; 038 private final Deque<String> routineIdStack; 039 private final Set<TFunctionCall> visitedFunctionCalls; 040 private final AnalyzerV2Config config; 041 private final String fileId; 042 private int batchCounter = 0; 043 044 public TsqlSymbolCollector(BoundProgram program, BoundScope globalScope, 045 AnalyzerV2Config config, String fileId) { 046 this.program = program; 047 this.scopeStack = new ArrayDeque<BoundScope>(); 048 this.routineIdStack = new ArrayDeque<String>(); 049 this.visitedFunctionCalls = new HashSet<TFunctionCall>(); 050 this.config = config; 051 this.fileId = fileId != null ? fileId : ""; 052 053 scopeStack.push(globalScope); 054 } 055 056 // ---- Scope helpers ---- 057 058 private BoundScope currentScope() { 059 return scopeStack.peek(); 060 } 061 062 private BoundScope pushScope(EScopeKind kind, SourceAnchor anchor) { 063 BoundScope scope = new BoundScope(kind, currentScope(), anchor); 064 scopeStack.push(scope); 065 program.addScope(scope); 066 return scope; 067 } 068 069 private void popScope() { 070 if (scopeStack.size() > 1) { 071 scopeStack.pop(); 072 } 073 } 074 075 private String currentRoutineId() { 076 return routineIdStack.isEmpty() ? null : routineIdStack.peek(); 077 } 078 079 private SourceAnchor anchor(TParseTreeNode node) { 080 SourceAnchor base = SourceAnchor.from(node); 081 if (base != null && !fileId.isEmpty()) { 082 return new SourceAnchor(fileId, 083 base.startOffset, base.endOffset, 084 base.startLine, base.startCol, 085 base.endLine, base.endCol, 086 base.statementKey, base.snippet); 087 } 088 return base; 089 } 090 091 // ========================================== 092 // Routine declaration visitors 093 // ========================================== 094 095 // ---- CREATE PROCEDURE ---- 096 // Note: TMssqlCreateProcedure extends TCreateProcedureStmt. 097 // The visitor dispatches to TCreateProcedureStmt, not TMssqlCreateProcedure. 098 099 @Override 100 public void preVisit(TCreateProcedureStmt node) { 101 // Only handle MSSQL procedures 102 if (node.dbvendor != EDbVendor.dbvmssql) return; 103 104 TObjectName procName = node.getStoredProcedureName(); 105 if (procName == null) return; 106 107 String rawName = procName.toString().trim(); 108 MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawName); 109 110 String schema = normalized.getSchema(); 111 String name = normalized.getObject(); 112 113 List<BoundParameterSymbol> params = extractParameters(node.getParameterDeclarations()); 114 115 BoundScope routineScope = pushScope(EScopeKind.ROUTINE, anchor(node)); 116 117 BoundRoutineSymbol routine = new BoundRoutineSymbol( 118 name.toUpperCase(), schema != null ? schema.toUpperCase() : null, 119 routineScope, anchor(node), params, null, ERoutineKind.PROCEDURE); 120 program.registerRoutine(routine); 121 routineIdStack.push(routine.getRoutineId()); 122 123 // Register params in scope 124 for (BoundParameterSymbol p : params) { 125 routineScope.declareSymbol(p.getParamName(), p); 126 } 127 } 128 129 @Override 130 public void postVisit(TCreateProcedureStmt node) { 131 if (node.dbvendor != EDbVendor.dbvmssql) return; 132 if (!routineIdStack.isEmpty()) routineIdStack.pop(); 133 popScope(); 134 } 135 136 // ---- CREATE FUNCTION ---- 137 138 @Override 139 public void preVisit(TCreateFunctionStmt node) { 140 if (!(node instanceof TMssqlCreateFunction)) return; 141 142 TObjectName funcName = node.getFunctionName(); 143 if (funcName == null) return; 144 145 String rawName = funcName.toString().trim(); 146 MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawName); 147 String schema = normalized.getSchema(); 148 String name = normalized.getObject(); 149 150 List<BoundParameterSymbol> params = extractParameters(node.getParameterDeclarations()); 151 152 ERoutineKind kind; 153 EFunctionReturnsType returnsType = node.getReturnsType(); 154 if (returnsType == EFunctionReturnsType.frtInlineTableValue) { 155 kind = ERoutineKind.FUNCTION; // IF 156 } else if (returnsType == EFunctionReturnsType.frtMultiStatementTableValue) { 157 kind = ERoutineKind.FUNCTION; // TF 158 } else { 159 kind = ERoutineKind.FUNCTION; // SF 160 } 161 162 BoundTypeRef returnType = MssqlTypeRefMapper.map(node.getReturnDataType()); 163 BoundScope routineScope = pushScope(EScopeKind.ROUTINE, anchor(node)); 164 165 BoundRoutineSymbol routine = new BoundRoutineSymbol( 166 name.toUpperCase(), schema != null ? schema.toUpperCase() : null, 167 routineScope, anchor(node), params, returnType, kind); 168 169 // Note: function sub-type (SF/IF/TF) not stored on BoundRoutineSymbol 170 // since it lacks a properties map. The kindCode() always returns "F". 171 // Sub-type can be inferred from the source anchor if needed. 172 173 program.registerRoutine(routine); 174 routineIdStack.push(routine.getRoutineId()); 175 176 for (BoundParameterSymbol p : params) { 177 routineScope.declareSymbol(p.getParamName(), p); 178 } 179 } 180 181 @Override 182 public void postVisit(TCreateFunctionStmt node) { 183 if (!(node instanceof TMssqlCreateFunction)) return; 184 if (!routineIdStack.isEmpty()) routineIdStack.pop(); 185 popScope(); 186 } 187 188 // ---- CREATE TRIGGER ---- 189 // Note: MSSQL triggers parse as TCreateTriggerStmt, not TMssqlCreateTrigger. 190 191 @Override 192 public void preVisit(TCreateTriggerStmt node) { 193 if (node.dbvendor != EDbVendor.dbvmssql) return; 194 195 TObjectName triggerName = node.getTriggerName(); 196 if (triggerName == null) return; 197 198 String rawName = triggerName.toString().trim(); 199 MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawName); 200 String schema = normalized.getSchema(); 201 String name = normalized.getObject(); 202 203 BoundScope routineScope = pushScope(EScopeKind.ROUTINE, anchor(node)); 204 205 BoundRoutineSymbol routine = new BoundRoutineSymbol( 206 name.toUpperCase(), schema != null ? schema.toUpperCase() : null, 207 routineScope, anchor(node), 208 Collections.<BoundParameterSymbol>emptyList(), null, ERoutineKind.TRIGGER); 209 210 // Track trigger table via object ref 211 TTable onTable = node.getOnTable(); 212 if (onTable != null) { 213 collectTableRef(onTable, null); 214 } 215 216 program.registerRoutine(routine); 217 routineIdStack.push(routine.getRoutineId()); 218 219 // Register inserted/deleted pseudo tables in trigger scope 220 routineScope.declareSymbol("INSERTED", 221 new BoundVariableSymbol("INSERTED", routineScope, null, null, false)); 222 routineScope.declareSymbol("DELETED", 223 new BoundVariableSymbol("DELETED", routineScope, null, null, false)); 224 } 225 226 @Override 227 public void postVisit(TCreateTriggerStmt node) { 228 if (node.dbvendor != EDbVendor.dbvmssql) return; 229 if (!routineIdStack.isEmpty()) routineIdStack.pop(); 230 popScope(); 231 } 232 233 // ========================================== 234 // Control flow / scope visitors 235 // ========================================== 236 237 @Override 238 public void preVisit(TMssqlBlock node) { 239 pushScope(EScopeKind.BLOCK, anchor(node)); 240 } 241 242 @Override 243 public void postVisit(TMssqlBlock node) { 244 popScope(); 245 } 246 247 @Override 248 public void preVisit(TMssqlTryCatch node) { 249 // TRY...CATCH treated as a single scope unit; calls inside 250 // inherit the current routine context (owningRoutineId). 251 // No extra scope push needed — the body statements are visited normally. 252 } 253 254 // ========================================== 255 // Variable declaration 256 // ========================================== 257 258 @Override 259 public void preVisit(TMssqlDeclare node) { 260 if (node.getVariables() != null) { 261 TDeclareVariableList vars = node.getVariables(); 262 for (int i = 0; i < vars.size(); i++) { 263 TDeclareVariable var = vars.getDeclareVariable(i); 264 if (var.getVariableName() != null) { 265 String varName = var.getVariableName().toString().trim(); 266 BoundTypeRef typeRef = var.getDatatype() != null 267 ? MssqlTypeRefMapper.map(var.getDatatype()) : null; 268 BoundVariableSymbol varSym = new BoundVariableSymbol( 269 varName, currentScope(), anchor(node), typeRef, false); 270 currentScope().declareSymbol(varName.toUpperCase(), varSym); 271 } 272 } 273 } 274 // Cursor declaration 275 if (node.getDeclareType() == EDeclareType.cursor && node.getCursorName() != null) { 276 String cursorName = node.getCursorName().toString().trim(); 277 BoundVariableSymbol cursorSym = new BoundVariableSymbol( 278 cursorName, currentScope(), anchor(node), 279 new BoundTypeRef("CURSOR", ETypeCategory.REF_CURSOR), false); 280 currentScope().declareSymbol(cursorName.toUpperCase(), cursorSym); 281 } 282 } 283 284 // ========================================== 285 // Call reference collection 286 // ========================================== 287 288 // ---- EXEC / EXECUTE ---- 289 290 @Override 291 public void preVisit(TMssqlExecute node) { 292 int execType = node.getExecType(); 293 294 if (execType == TBaseType.metExecSp || execType == TBaseType.metNoExecKeyword) { 295 handleExecModule(node); 296 } else if (execType == TBaseType.metExecStringCmd 297 || execType == TBaseType.metExecStringCmdLinkServer) { 298 handleDynamicExecString(node); 299 } 300 } 301 302 private void handleExecModule(TMssqlExecute node) { 303 TObjectName moduleName = node.getModuleName(); 304 if (moduleName == null) return; 305 306 String rawName = moduleName.toString().trim(); 307 if (rawName.isEmpty()) return; 308 309 MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawName); 310 311 // Cross-database or remote call → external dependency 312 if (normalized.getPartCount() >= 4) { 313 addExternalRef(rawName, "REMOTE_CALL", node); 314 return; 315 } 316 if (normalized.getPartCount() >= 3 && normalized.getDatabase() != null) { 317 addExternalRef(rawName, "CROSS_DATABASE", node); 318 return; 319 } 320 321 // Check if sp_executesql 322 if (MssqlExternalDepClassifier.isSpExecuteSql(rawName)) { 323 addExternalRef(rawName, "SYSTEM_PROC", node); 324 handleSpExecuteSql(node); 325 return; 326 } 327 328 // System / extended proc 329 if (MssqlExternalDepClassifier.isExternal(rawName)) { 330 String extType = MssqlExternalDepClassifier.classify(rawName); 331 addExternalRef(rawName, extType, node); 332 return; 333 } 334 335 // User procedure call 336 int argCount = node.getParameters() != null ? node.getParameters().size() : 0; 337 addRoutineRef(rawName, argCount, node, Confidence.HIGH, "EXEC module"); 338 } 339 340 private void handleSpExecuteSql(TMssqlExecute node) { 341 // Try to extract the SQL literal from the first parameter 342 String sqlText = null; 343 if (node.getParameters() != null && node.getParameters().size() > 0) { 344 TExecParameter firstParam = node.getParameters().getExecParameter(0); 345 if (firstParam != null && firstParam.getParameterValue() != null) { 346 sqlText = extractStringLiteral(firstParam.getParameterValue()); 347 } 348 } 349 // Also check node.getSqlText() which the parser may have resolved 350 if (sqlText == null) { 351 sqlText = node.getSqlText(); 352 } 353 354 if (sqlText != null && !sqlText.isEmpty()) { 355 parseInnerSql(sqlText); 356 } 357 } 358 359 private void handleDynamicExecString(TMssqlExecute node) { 360 // EXEC('sql string') or EXEC(@variable) 361 String sqlText = null; 362 if (node.getStringValues() != null && node.getStringValues().size() > 0) { 363 TExpression firstExpr = node.getStringValues().getExpression(0); 364 if (firstExpr != null) { 365 sqlText = extractStringLiteral(firstExpr); 366 } 367 } 368 if (sqlText == null) { 369 sqlText = node.getSqlText(); 370 } 371 372 if (sqlText != null && !sqlText.isEmpty()) { 373 parseInnerSql(sqlText); 374 } 375 376 // Record dynamic SQL as external dependency 377 BoundRoutineRef ref = createRoutineRef( 378 "EXEC_DYNAMIC_SQL", 0, node, Confidence.LOW, "EXEC string"); 379 ref.setProperty("externalDependency", true); 380 ref.setProperty("externalType", "DYNAMIC_SQL"); 381 program.addRoutineRef(ref); 382 } 383 384 /** 385 * Attempts to parse inner SQL from dynamic SQL and extract refs. 386 */ 387 private void parseInnerSql(String sqlText) { 388 try { 389 TGSqlParser innerParser = new TGSqlParser(EDbVendor.dbvmssql); 390 innerParser.sqltext = sqlText; 391 if (innerParser.parse() == 0) { 392 // Extract table refs from inner SQL 393 for (int i = 0; i < innerParser.sqlstatements.size(); i++) { 394 TCustomSqlStatement stmt = innerParser.sqlstatements.get(i); 395 if (stmt != null && stmt.tables != null) { 396 for (int j = 0; j < stmt.tables.size(); j++) { 397 TTable table = stmt.tables.getTable(j); 398 collectTableRef(table, stmt); 399 } 400 } 401 } 402 } 403 } catch (Exception e) { 404 // Ignore parse failures in dynamic SQL 405 } 406 } 407 408 // ---- SET @var = expr (expression not traversed by TMssqlSet.acceptChildren) ---- 409 410 @Override 411 public void preVisit(TMssqlSet node) { 412 if (node.getSetType() == TBaseType.mstLocalVar && node.getVarExpr() != null) { 413 // TMssqlSet.acceptChildren() does not traverse varExpr, 414 // so function calls in SET assignments are missed. 415 // Manually traverse the expression tree to pick up TFunctionCall nodes. 416 node.getVarExpr().acceptChildren(this); 417 } 418 } 419 420 // ---- Function calls in expressions ---- 421 422 @Override 423 public void preVisit(TFunctionCall funcCall) { 424 if (visitedFunctionCalls.contains(funcCall)) return; 425 visitedFunctionCalls.add(funcCall); 426 427 if (funcCall.getFunctionName() == null) return; 428 String funcName = funcCall.getFunctionName().toString().trim(); 429 if (funcName.isEmpty()) return; 430 431 // Filter XML data type methods (.nodes/.value/.query/.exist/.modify) 432 // and .WRITE() partial column update. These produce TFunctionCall nodes 433 // but are NOT user-defined function calls. Only filter when the name 434 // has a dot prefix (e.g., "alias.value", "@var.nodes", "col.WRITE"). 435 int lastDot = funcName.lastIndexOf('.'); 436 if (lastDot > 0) { 437 String lastPart = funcName.substring(lastDot + 1).toUpperCase(); 438 if (DOT_METHOD_NAMES.contains(lastPart)) { 439 return; 440 } 441 } 442 443 // Skip known aggregate/window functions that the parser handles 444 EFunctionType ftype = funcCall.getFunctionType(); 445 if (isParserHandledFunction(ftype)) { 446 // These are SQL-level aggregates/window functions, not user UDF calls. 447 // Still mark as external if they are built-in. 448 if (MssqlExternalDepClassifier.isExternal(funcName)) { 449 addExternalRef(funcName, MssqlExternalDepClassifier.classify(funcName), funcCall); 450 } 451 return; 452 } 453 454 MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(funcName); 455 456 // Cross-database/remote → external 457 if (normalized.getPartCount() >= 3 && normalized.getDatabase() != null) { 458 addExternalRef(funcName, "CROSS_DATABASE", funcCall); 459 return; 460 } 461 462 // Check if known external 463 if (MssqlExternalDepClassifier.isExternal(funcName)) { 464 addExternalRef(funcName, MssqlExternalDepClassifier.classify(funcName), funcCall); 465 return; 466 } 467 468 // Check if it's a declared variable (not a function call) 469 String objectPart = normalized.getObject(); 470 if (objectPart.startsWith("@")) { 471 // @variable is not a function call 472 return; 473 } 474 475 // User-defined function call 476 int argCount = 0; 477 if (funcCall.getArgs() != null) { 478 argCount = funcCall.getArgs().size(); 479 } 480 addRoutineRef(funcName, argCount, funcCall, Confidence.HIGH, "function call in expression"); 481 } 482 483 private boolean isParserHandledFunction(EFunctionType ftype) { 484 if (ftype == null) return false; 485 switch (ftype) { 486 case cast_t: 487 case convert_t: 488 case trim_t: 489 case extract_t: 490 case treat_t: 491 case contains_t: 492 case freetext_t: 493 case rank_t: 494 case builtin_t: 495 return true; 496 default: 497 return false; 498 } 499 } 500 501 // ========================================== 502 // Table reference collection 503 // ========================================== 504 505 @Override 506 public void preVisit(TSelectSqlStatement node) { 507 // Only process leaf SELECTs (not UNION containers) 508 if (node.getSetOperatorType() != ESetOperatorType.none) return; 509 collectTablesFromStatement(node); 510 } 511 512 @Override 513 public void preVisit(TInsertSqlStatement node) { 514 collectTablesFromStatement(node); 515 } 516 517 @Override 518 public void preVisit(TUpdateSqlStatement node) { 519 collectTablesFromStatement(node); 520 } 521 522 @Override 523 public void preVisit(TDeleteSqlStatement node) { 524 collectTablesFromStatement(node); 525 } 526 527 @Override 528 public void preVisit(TMergeSqlStatement node) { 529 collectTablesFromStatement(node); 530 } 531 532 private void collectTablesFromStatement(TCustomSqlStatement stmt) { 533 if (stmt == null || stmt.tables == null) return; 534 for (int i = 0; i < stmt.tables.size(); i++) { 535 TTable table = stmt.tables.getTable(i); 536 collectTableRef(table, stmt); 537 } 538 } 539 540 private void collectTableRef(TTable table, TCustomSqlStatement stmt) { 541 if (table == null || table.getTableName() == null) return; 542 String tableName = table.getTableName().toString().trim(); 543 if (tableName.isEmpty()) return; 544 545 // Classify table 546 String category = classifyTable(tableName, table); 547 548 List<String> nameParts = MssqlNameNormalizer.splitParts(tableName); 549 for (int i = 0; i < nameParts.size(); i++) { 550 nameParts.set(i, MssqlNameNormalizer.stripQuotes(nameParts.get(i))); 551 } 552 553 EObjectRefKind refKind = mapTableType(table.getTableType()); 554 555 BoundObjectRef objRef = new BoundObjectRef( 556 tableName, nameParts, 557 EBindingStatus.UNRESOLVED_SOFT, null, null, 558 refKind, null); 559 objRef.setSourceAnchor(anchor(table)); 560 objRef.setProperty("owningRoutineId", currentRoutineId()); 561 562 // Set table effect type 563 ETableEffectType effectType = table.getEffectType(); 564 if (effectType != null) { 565 objRef.setProperty("tableEffectType", effectType); 566 } 567 // Fallback: statement type 568 if (stmt != null) { 569 objRef.setProperty("statementType", stmt.sqlstatementtype); 570 } 571 572 // Mssql table category 573 objRef.setProperty("mssql.tableCategory", category); 574 575 program.addObjectRef(objRef); 576 } 577 578 private String classifyTable(String tableName, TTable table) { 579 String upper = tableName.toUpperCase().trim(); 580 String stripped = MssqlNameNormalizer.stripQuotes(upper); 581 // Check last part for prefix patterns 582 int dot = stripped.lastIndexOf('.'); 583 String objectPart = dot >= 0 ? stripped.substring(dot + 1) : stripped; 584 585 if (objectPart.equals("INSERTED") || objectPart.equals("DELETED")) { 586 return "PSEUDO"; 587 } 588 if (objectPart.startsWith("##")) { 589 return "GLOBAL_TEMP"; 590 } 591 if (objectPart.startsWith("#")) { 592 return "TEMP"; 593 } 594 if (objectPart.startsWith("@")) { 595 return "TABLE_VARIABLE"; 596 } 597 return "REAL"; 598 } 599 600 private EObjectRefKind mapTableType(ETableSource tableType) { 601 if (tableType == null) return EObjectRefKind.TABLE; 602 switch (tableType) { 603 case subquery: 604 return EObjectRefKind.DERIVED_TABLE; 605 case function: 606 return EObjectRefKind.TABLE_FUNCTION; 607 case pivoted_table: 608 return EObjectRefKind.PIVOT_TABLE; 609 default: 610 return EObjectRefKind.TABLE; 611 } 612 } 613 614 // ========================================== 615 // Anonymous batch handling 616 // ========================================== 617 618 /** 619 * Called for top-level DML statements that are not inside any routine definition. 620 * These are grouped as anonymous batches. 621 */ 622 private void ensureAnonymousBatchContext(TParseTreeNode node) { 623 if (currentRoutineId() == null) { 624 // Create anonymous batch routine 625 batchCounter++; 626 String batchName = "<batch_" + batchCounter + ">"; 627 628 BoundScope batchScope = pushScope(EScopeKind.ROUTINE, anchor(node)); 629 BoundRoutineSymbol batch = new BoundRoutineSymbol( 630 batchName, null, batchScope, anchor(node), 631 Collections.<BoundParameterSymbol>emptyList(), null, 632 ERoutineKind.ANONYMOUS_BLOCK); 633 program.registerRoutine(batch); 634 routineIdStack.push(batch.getRoutineId()); 635 } 636 } 637 638 // ========================================== 639 // Helper methods 640 // ========================================== 641 642 private List<BoundParameterSymbol> extractParameters(TParameterDeclarationList paramList) { 643 List<BoundParameterSymbol> params = new ArrayList<BoundParameterSymbol>(); 644 if (paramList == null) return params; 645 646 for (int i = 0; i < paramList.size(); i++) { 647 TParameterDeclaration pd = paramList.getParameterDeclarationItem(i); 648 if (pd == null || pd.getParameterName() == null) continue; 649 650 String paramName = pd.getParameterName().toString().trim(); 651 BoundTypeRef typeRef = MssqlTypeRefMapper.map(pd.getDataType()); 652 653 gudusoft.gsqlparser.ir.bound.EParameterMode mode = 654 gudusoft.gsqlparser.ir.bound.EParameterMode.IN; 655 gudusoft.gsqlparser.EParameterMode pdMode = pd.getParameterMode(); 656 if (pdMode == gudusoft.gsqlparser.EParameterMode.output 657 || pdMode == gudusoft.gsqlparser.EParameterMode.out 658 || pdMode == gudusoft.gsqlparser.EParameterMode.inout) { 659 mode = gudusoft.gsqlparser.ir.bound.EParameterMode.IN_OUT; 660 } 661 662 BoundParameterSymbol param = new BoundParameterSymbol( 663 paramName, null, anchor(pd), typeRef, mode); 664 params.add(param); 665 } 666 return params; 667 } 668 669 private void addRoutineRef(String rawName, int argCount, TParseTreeNode node, 670 Confidence confidence, String evidenceMsg) { 671 BoundRoutineRef ref = createRoutineRef(rawName, argCount, node, confidence, evidenceMsg); 672 program.addRoutineRef(ref); 673 } 674 675 private BoundRoutineRef createRoutineRef(String rawName, int argCount, 676 TParseTreeNode node, 677 Confidence confidence, String evidenceMsg) { 678 List<String> nameParts = MssqlNameNormalizer.splitParts(rawName); 679 for (int i = 0; i < nameParts.size(); i++) { 680 nameParts.set(i, MssqlNameNormalizer.stripQuotes(nameParts.get(i))); 681 } 682 683 Evidence evidence = new Evidence(EvidenceKind.STATIC_RESOLVED, evidenceMsg); 684 BoundRoutineRef ref = new BoundRoutineRef( 685 rawName, nameParts, 686 EBindingStatus.UNRESOLVED_SOFT, null, null, 687 Collections.<BoundArgument>emptyList(), 688 evidence, confidence); 689 ref.setSourceAnchor(anchor(node)); 690 ref.setProperty("owningRoutineId", currentRoutineId()); 691 ref.setProperty("argCount", argCount); 692 return ref; 693 } 694 695 private void addExternalRef(String rawName, String externalType, TParseTreeNode node) { 696 BoundRoutineRef ref = createRoutineRef(rawName, 0, node, Confidence.HIGH, 697 "External: " + externalType + " " + rawName); 698 ref.setProperty("externalDependency", true); 699 ref.setProperty("externalType", externalType); 700 ref.setProperty("externalName", MssqlNameNormalizer.stripQuotes(rawName)); 701 if (MssqlExternalDepClassifier.isSecuritySensitive(rawName)) { 702 ref.setProperty("securitySensitive", true); 703 } 704 program.addRoutineRef(ref); 705 } 706 707 private String extractStringLiteral(TExpression expr) { 708 if (expr == null) return null; 709 String text = expr.toString().trim(); 710 // Strip N prefix and surrounding quotes 711 if (text.toUpperCase().startsWith("N'") && text.endsWith("'") && text.length() >= 3) { 712 return text.substring(2, text.length() - 1).replace("''", "'"); 713 } 714 if (text.startsWith("'") && text.endsWith("'") && text.length() >= 2) { 715 return text.substring(1, text.length() - 1).replace("''", "'"); 716 } 717 return null; 718 } 719}