001package gudusoft.gsqlparser.sqlcmds; 002 003import gudusoft.gsqlparser.*; 004import gudusoft.gsqlparser.stmt.*; 005 006/** 007 * Abstract base class for vendor-specific SQL command resolvers. 008 * Provides common functionality and the TSqlCmd/TSqlCmdList infrastructure 009 * that all vendor implementations share. 010 * 011 * @since 3.1.0.9 012 */ 013public abstract class AbstractSqlCmds implements ISqlCmds { 014 015 protected EDbVendor vendor; 016 protected volatile TSqlCmdList sqlCmdList; 017 protected final Object initLock = new Object(); 018 protected volatile boolean initialized = false; 019 020 /** 021 * Temporary field for tracking statement type during issql() processing. 022 * Used by both findcte() and issql() methods. 023 */ 024 protected ESqlStatementType gnewsqlstatementtype = ESqlStatementType.sstinvalid; 025 026 /** 027 * Constructor for vendor-specific implementation. 028 * @param vendor Database vendor this resolver handles 029 */ 030 protected AbstractSqlCmds(EDbVendor vendor) { 031 this.vendor = vendor; 032 } 033 034 @Override 035 public EDbVendor getVendor() { 036 return vendor; 037 } 038 039 040 @Override 041 public TSqlCmdList getSqlCmdList() { 042 ensureInitialized(); 043 return sqlCmdList; 044 } 045 046 /** 047 * Ensures command list is initialized using double-checked locking. 048 * This provides thread-safe lazy initialization. 049 */ 050 protected void ensureInitialized() { 051 if (!initialized) { 052 synchronized (initLock) { 053 if (!initialized) { 054 sqlCmdList = new TSqlCmdList(); 055 initializeCommands(); 056 initialized = true; 057 } 058 } 059 } 060 } 061 062 /** 063 * Initialize vendor-specific commands. 064 * Subclasses must implement this to populate sqlCmdList. 065 */ 066 protected abstract void initializeCommands(); 067 068 /** 069 * Helper method to add a command to the list. 070 * Simplifies command initialization in subclasses. 071 * 072 * @param token1 First token (keyword) 073 * @param token2 Second token pattern 074 * @param token3 Third token pattern 075 * @param token4 Fourth token pattern 076 * @param token5 Fifth token pattern 077 * @param token6 Sixth token pattern 078 * @param token7 Seventh token pattern 079 * @param token8 Eighth token pattern 080 * @param stmtType Statement type 081 */ 082 protected void addCmd(int token1, String token2, String token3, String token4, 083 String token5, String token6, String token7, String token8, 084 ESqlStatementType stmtType) { 085 TSqlCmd cmd = new TSqlCmd(); 086 cmd.token1 = token1; 087 cmd.token2 = token2; 088 cmd.token3 = token3; 089 cmd.token4 = token4; 090 cmd.token5 = token5; 091 cmd.token6 = token6; 092 cmd.token7 = token7; 093 cmd.token8 = token8; 094 cmd.sqlstatementtype = stmtType; 095 096 // Handle vendor-specific reserved words (token codes > TBaseType.rrw_abort) 097 // These need their string representation set explicitly 098 if (token1 > TBaseType.rrw_abort) { 099 String tokenStr = getToken1Str(token1); 100 if (tokenStr != null && !tokenStr.isEmpty()) { 101 cmd.token1Str = tokenStr; 102 } 103 } 104 105 sqlCmdList.add(cmd); 106 } 107 108 /** 109 * Replace the statement type of an inherited registration with the same 110 * token pattern, in place. Subclasses use this when they must change what 111 * a head maps to: a plain re-registration would only append a duplicate 112 * that loses the first-registered tie in finddbcmd, and re-ordering the 113 * registrations would break the token1 grouping the lookup relies on. 114 * Falls back to a normal append when no matching entry exists. 115 */ 116 protected void overrideCmd(int token1, String token2, String token3, ESqlStatementType stmtType) { 117 for (int i = 0; i < sqlCmdList.size(); i++) { 118 TSqlCmd cmd = (TSqlCmd) sqlCmdList.get(i); 119 if ((cmd.token1 == token1) 120 && wordMatches(cmd.token2, token2) 121 && wordMatches(cmd.token3, token3) 122 && isBlankWord(cmd.token4) && isBlankWord(cmd.token5) 123 && isBlankWord(cmd.token6) && isBlankWord(cmd.token7)) { 124 cmd.sqlstatementtype = stmtType; 125 return; 126 } 127 } 128 addCmd(token1, token2, token3, "", "", "", "", "", stmtType); 129 } 130 131 private static boolean isBlankWord(String w) { 132 return (w == null) || w.trim().isEmpty(); 133 } 134 135 private static boolean wordMatches(String stored, String requested) { 136 if (isBlankWord(requested)) return isBlankWord(stored); 137 return (stored != null) && stored.trim().equalsIgnoreCase(requested.trim()); 138 } 139 140 /** 141 * Get the string representation for vendor-specific token codes. 142 * Subclasses should override this method to provide mappings for 143 * vendor-specific reserved words (tokens > TBaseType.rrw_abort). 144 * 145 * @param token1 Token code 146 * @return String representation of the token, or null if not applicable 147 */ 148 protected String getToken1Str(int token1) { 149 // Default implementation returns null 150 // Subclasses override to provide vendor-specific mappings 151 return null; 152 } 153 154 /** 155 * Overloaded helper for simpler commands. 156 */ 157 protected void addCmd(int token1, ESqlStatementType stmtType) { 158 addCmd(token1, "", "", "", "", "", "", "", stmtType); 159 } 160 161 protected void addCmd(int token1, String token2, ESqlStatementType stmtType) { 162 addCmd(token1, token2, "", "", "", "", "", "", stmtType); 163 } 164 165 protected void addCmd(int token1, String token2, String token3, ESqlStatementType stmtType) { 166 addCmd(token1, token2, token3, "", "", "", "", "", stmtType); 167 } 168 169 protected void addCmd(int token1, String token2, String token3, String token4, ESqlStatementType stmtType) { 170 addCmd(token1, token2, token3, token4, "", "", "", "", stmtType); 171 } 172 173 protected void addCmd(int token1, String token2, String token3, String token4, String token5, ESqlStatementType stmtType) { 174 addCmd(token1, token2, token3, token4, token5, "", "", "", stmtType); 175 } 176 177 protected void addCmd(int token1, String token2, String token3, String token4, String token5, String token6, ESqlStatementType stmtType) { 178 addCmd(token1, token2, token3, token4, token5, token6, "", "", stmtType); 179 } 180 181 protected void addCmd(int token1, String token2, String token3, String token4, String token5, String token6, String token7, ESqlStatementType stmtType) { 182 addCmd(token1, token2, token3, token4, token5, token6, token7, "", stmtType); 183 } 184 185 /** 186 * Find command in the list starting from a specific token. 187 * This is the core search algorithm used by all vendors. 188 * 189 * @param pcst Starting token 190 * @param cmdList Command list to search 191 * @return Found command or null 192 */ 193 protected TSqlCmd finddbcmd(TSourceToken pcst, TSqlCmdList cmdList) { 194 if (pcst == null || cmdList == null) return null; 195 if (pcst.tokentype != ETokenType.ttkeyword) return null; 196 197 int startIndex = cmdList.getStartIndex(pcst.tokencode); 198 if (startIndex == -1) return null; 199 200 TSqlCmd bestMatch = null; 201 int maxTokensMatched = 0; 202 203 for (int i = startIndex; i < cmdList.size(); i++) { 204 TSqlCmd cmd = (TSqlCmd) cmdList.get(i); 205 206 // First token must match 207 if (cmd.token1 != pcst.tokencode) { 208 // If we've already found some matches, we can break since commands are grouped by first token 209 if (maxTokensMatched > 0) break; 210 continue; 211 } 212 213 int tokensMatched = 1; 214 TSourceToken lcst = pcst; 215 boolean matches = true; 216 217 // Check token2 218 if (!cmd.token2.isEmpty() && !cmd.token2.equals(" ")) { 219 lcst = lcst.nextSolidToken(); 220 if (!tokenMatches(lcst, cmd.token2)) { 221 matches = false; 222 } else { 223 tokensMatched = 2; 224 } 225 } 226 227 // Check token3 228 if (matches && !cmd.token3.isEmpty() && !cmd.token3.equals(" ")) { 229 lcst = lcst.nextSolidToken(); 230 if (!tokenMatches(lcst, cmd.token3)) { 231 matches = false; 232 } else { 233 tokensMatched = 3; 234 } 235 } 236 237 // Check token4 238 if (matches && !cmd.token4.isEmpty() && !cmd.token4.equals(" ")) { 239 lcst = lcst.nextSolidToken(); 240 if (!tokenMatches(lcst, cmd.token4)) { 241 matches = false; 242 } else { 243 tokensMatched = 4; 244 } 245 } 246 247 // Check token5 248 if (matches && !cmd.token5.isEmpty() && !cmd.token5.equals(" ")) { 249 lcst = lcst.nextSolidToken(); 250 if (!tokenMatches(lcst, cmd.token5)) { 251 matches = false; 252 } else { 253 tokensMatched = 5; 254 } 255 } 256 257 // Check token6 258 if (matches && !cmd.token6.isEmpty() && !cmd.token6.equals(" ")) { 259 lcst = lcst.nextSolidToken(); 260 if (!tokenMatches(lcst, cmd.token6)) { 261 matches = false; 262 } else { 263 tokensMatched = 6; 264 } 265 } 266 267 // Check token7 268 if (matches && !cmd.token7.isEmpty() && !cmd.token7.equals(" ")) { 269 lcst = lcst.nextSolidToken(); 270 if (!tokenMatches(lcst, cmd.token7)) { 271 matches = false; 272 } else { 273 tokensMatched = 7; 274 } 275 } 276 277 // Check token8 278 if (matches && !cmd.token8.isEmpty() && !cmd.token8.equals(" ")) { 279 lcst = lcst.nextSolidToken(); 280 if (!tokenMatches(lcst, cmd.token8)) { 281 matches = false; 282 } else { 283 tokensMatched = 8; 284 } 285 } 286 287 // If this command matches and has more tokens than our current best match, update it 288 if (matches && tokensMatched > maxTokensMatched) { 289 bestMatch = cmd; 290 maxTokensMatched = tokensMatched; 291 292 // If this command ends with an empty token (exact match), we can return immediately 293 // as no longer match is possible 294 if ((tokensMatched == 2 && cmd.token3.isEmpty()) || 295 (tokensMatched == 3 && cmd.token4.isEmpty()) || 296 (tokensMatched == 4 && cmd.token5.isEmpty()) || 297 (tokensMatched == 5 && cmd.token6.isEmpty()) || 298 (tokensMatched == 6 && cmd.token7.isEmpty()) || 299 (tokensMatched == 7 && cmd.token8.isEmpty()) || 300 (tokensMatched == 8)) { 301 return bestMatch; 302 } 303 } 304 } 305 306 return bestMatch; 307 } 308 309 /** 310 * Check if a token matches a pattern string. 311 * Pattern can be "*" (any), or a literal string. 312 */ 313 private boolean tokenMatches(TSourceToken token, String pattern) { 314 if (token == null) return false; 315 if (pattern.equals("*")) return true; 316 317 // Check literal match (case-insensitive) 318 return token.toString().equalsIgnoreCase(pattern); 319 } 320 321 @Override 322 public ESqlStatementType getStatementTypeForToken(TSourceToken token) { 323 ensureInitialized(); 324 TSqlCmd cmd = finddbcmd(token, sqlCmdList); 325 return (cmd != null) ? cmd.sqlstatementtype : ESqlStatementType.sstinvalid; 326 } 327 328 /** 329 * Detect Common Table Expression (CTE) for all database vendors. 330 * Handles WITH clauses before DELETE, INSERT, SELECT, UPDATE, MERGE. 331 * 332 * This is a shared method used by all vendor implementations since CTE 333 * syntax is similar across vendors (standardized by SQL:1999). 334 * 335 * @param ptoken The WITH token 336 * @return Statement object for the CTE query, or null if not a valid CTE 337 */ 338 protected TCustomSqlStatement findcte(TSourceToken ptoken) { 339 TCustomSqlStatement ret = null; 340 TSourceToken lctoken = null; 341 int lcnested = 0, k, j; 342 boolean inXmlNamespaces = false; 343 boolean isXmlNamespaces = false; 344 345 int lcpos = ptoken.posinlist; 346 TSourceTokenList lcsourcetokenlist = ptoken.container; 347 348 ret = findCteByStructure(ptoken); 349 if (ret != null) return ret; 350 351 for (int i = lcpos + 1; i < lcsourcetokenlist.size(); i++) { 352 lctoken = lcsourcetokenlist.get(i); 353 354 // Handle XML namespaces (for SQL Server compatibility) 355 if (lctoken.tokencode == TBaseType.rrw_xmlnamespaces) { 356 inXmlNamespaces = true; 357 lcnested = 0; 358 continue; 359 } 360 361 if (inXmlNamespaces) { 362 if (lctoken.tokentype == ETokenType.ttleftparenthesis) lcnested++; 363 if (lctoken.tokentype == ETokenType.ttrightparenthesis) { 364 lcnested--; 365 if (lcnested == 0) { 366 inXmlNamespaces = false; 367 isXmlNamespaces = true; 368 } 369 } 370 continue; 371 } 372 373 // Look for AS keyword or after XML namespaces 374 if ((lctoken.tokencode == TBaseType.rrw_as) || isXmlNamespaces) { 375 lcnested = 0; 376 int startPos = i + 1; 377 if (isXmlNamespaces) startPos = i; 378 379 for (j = startPos; j < lcsourcetokenlist.size(); j++) { 380 lctoken = lcsourcetokenlist.get(j); 381 if (lctoken.isnonsolidtoken()) continue; 382 if (lctoken.tokentype == ETokenType.ttleftparenthesis) lcnested++; 383 if (lctoken.tokentype == ETokenType.ttrightparenthesis) lcnested--; 384 385 if (lcnested == 0) { 386 ret = createCteMainStatement(lctoken); 387 if (ret != null) break; 388 } 389 } 390 391 // Mark CTE tokens as ignored for raw statement processing 392 if (ret != null) { 393 for (k = lcpos + 1; k <= j; k++) { 394 lcsourcetokenlist.get(k).tokenstatus = ETokenStatus.tsignoredbygetrawstatement; 395 } 396 break; 397 } 398 } 399 } 400 401 return ret; 402 } 403 404 /** 405 * Detect a CTE query by structurally walking over the whole CTE list of the 406 * WITH clause instead of scanning for the first statement keyword that sits 407 * at parenthesis depth zero. 408 * 409 * <p>The keyword scan cannot see a main query that is wrapped in its own 410 * parentheses — <code>WITH c AS (SELECT ...) (SELECT ...)</code> — because 411 * that SELECT never appears at depth zero. It then keeps retrying from every 412 * later AS token, which either finds nothing (the WITH clause is reported as 413 * a tokenizer error) or latches onto a keyword inside the CTE body and builds 414 * the wrong statement class. 415 * 416 * <p>Vendors that keep their own copy of the legacy scan call this method 417 * first and fall back to that copy when it returns null. 418 * 419 * @param withToken the WITH token that opens the clause 420 * @return the statement for the main query, or null when the CTE list does 421 * not have the standard shape or the main query does not start with 422 * a keyword that may follow a CTE list 423 */ 424 protected TCustomSqlStatement findCteByStructure(TSourceToken withToken) { 425 TSourceToken mainToken = findMainQueryTokenAfterCteList(withToken); 426 if (mainToken == null) return null; 427 428 TCustomSqlStatement ret = createCteMainStatement(mainToken); 429 if (ret == null) return null; 430 431 TSourceTokenList list = withToken.container; 432 for (int k = withToken.posinlist + 1; k <= mainToken.posinlist; k++) { 433 list.get(k).tokenstatus = ETokenStatus.tsignoredbygetrawstatement; 434 } 435 return ret; 436 } 437 438 /** 439 * Build the statement object for the main query that follows a WITH clause, 440 * based on the keyword that opens it. 441 * 442 * @param lctoken first keyword of the main query (SELECT, INSERT, ...) 443 * @return the statement for that main query, or null if the token does not 444 * open a statement that may follow a CTE list 445 */ 446 private TCustomSqlStatement createCteMainStatement(TSourceToken lctoken) { 447 TSourceTokenList lcsourcetokenlist = lctoken.container; 448 TCustomSqlStatement ret = null; 449 int k; 450 451 if (lctoken.tokencode == TBaseType.rrw_delete) { 452 ret = new TDeleteSqlStatement(vendor); 453 ret.isctequery = true; 454 gnewsqlstatementtype = ESqlStatementType.sstdelete; 455 return ret; 456 } 457 458 if (lctoken.tokencode == TBaseType.rrw_merge) { 459 ret = new TMergeSqlStatement(vendor); 460 ret.isctequery = true; 461 gnewsqlstatementtype = ESqlStatementType.sstmerge; 462 return ret; 463 } 464 465 if ((lctoken.tokencode == TBaseType.rrw_insert) || (lctoken.tokencode == TBaseType.rrw_replace)) { 466 ret = new TInsertSqlStatement(vendor); 467 ret.isctequery = true; 468 gnewsqlstatementtype = (lctoken.tokencode == TBaseType.rrw_replace) ? ESqlStatementType.sstmysqlreplace : ESqlStatementType.sstinsert; 469 ret.sqlstatementtype = gnewsqlstatementtype; 470 ret.dummytag = 1; // select stmt in insert is permitted 471 472 for (k = lctoken.posinlist + 1; k < lcsourcetokenlist.size(); k++) { 473 if (lcsourcetokenlist.get(k).isnonsolidtoken()) continue; 474 if (lcsourcetokenlist.get(k).tokencode == TBaseType.rrw_values) break; 475 if (lcsourcetokenlist.get(k).tokencode == TBaseType.rrw_go) break; 476 if (lcsourcetokenlist.get(k).tokentype == ETokenType.ttsemicolon) break; 477 if (lcsourcetokenlist.get(k).tokencode == TBaseType.rrw_select) break; 478 if (lcsourcetokenlist.get(k).tokencode == TBaseType.rrw_teradata_sel) break; 479 if (lcsourcetokenlist.get(k).tokencode == TBaseType.rrw_execute) break; 480 if (lcsourcetokenlist.get(k).tokencode == TBaseType.rrw_exec) break; 481 } 482 if (k > lcsourcetokenlist.size() - 1) 483 k = lcsourcetokenlist.size() - 1; 484 485 for (int m = lctoken.posinlist + 1; m <= k; m++) { 486 lcsourcetokenlist.get(m).tokenstatus = ETokenStatus.tsignoredbygetrawstatement; 487 } 488 489 return ret; 490 } 491 492 if ((lctoken.tokencode == TBaseType.rrw_values) && (vendor == EDbVendor.dbvpostgresql || vendor == EDbVendor.dbvmysql)) { 493 ret = new TSelectSqlStatement(vendor); 494 ret.isctequery = true; 495 gnewsqlstatementtype = ESqlStatementType.sstselect; 496 return ret; 497 } 498 499 // MySQL TABLE statement (TABLE t1 is equivalent to SELECT * FROM t1) 500 if ((lctoken.tokencode == TBaseType.rrw_table) && (vendor == EDbVendor.dbvmysql)) { 501 ret = new TSelectSqlStatement(vendor); 502 ret.isctequery = true; 503 gnewsqlstatementtype = ESqlStatementType.sstselect; 504 return ret; 505 } 506 507 if (lctoken.tokencode == TBaseType.rrw_select) { 508 ret = new TSelectSqlStatement(vendor); 509 ret.isctequery = true; 510 gnewsqlstatementtype = ESqlStatementType.sstselect; 511 return ret; 512 } 513 514 // Teradata SEL keyword (abbreviation for SELECT) 515 if (lctoken.tokencode == TBaseType.rrw_teradata_sel) { 516 ret = new TSelectSqlStatement(vendor); 517 ret.isctequery = true; 518 gnewsqlstatementtype = ESqlStatementType.sstselect; 519 return ret; 520 } 521 522 if (lctoken.tokencode == TBaseType.rrw_update) { 523 ret = new TUpdateSqlStatement(vendor); 524 ret.isctequery = true; 525 ret.dummytag = 1; // means set clause in update is not found yet 526 gnewsqlstatementtype = ESqlStatementType.sstupdate; 527 return ret; 528 } 529 530 if ((vendor == EDbVendor.dbvhive) && (lctoken.tokencode == TBaseType.rrw_from)) { 531 TSourceToken cmdToken = lctoken.searchToken(TBaseType.rrw_insert, 3); 532 if (cmdToken != null) { 533 ret = new TInsertSqlStatement(vendor); 534 ret.isctequery = true; 535 gnewsqlstatementtype = ESqlStatementType.sstinsert; 536 } else { 537 ret = new TSelectSqlStatement(vendor); 538 ret.isctequery = true; 539 gnewsqlstatementtype = ESqlStatementType.ssthiveFromQuery; 540 } 541 return ret; 542 } 543 544 // BigQuery: CTE followed by FROM pipe syntax (WITH ... FROM t |> ...) 545 if ((vendor == EDbVendor.dbvbigquery) && (lctoken.tokencode == TBaseType.rrw_from)) { 546 ret = new TUnknownSqlStatement(vendor); 547 ret.isctequery = true; 548 ret.sqlstatementtype = ESqlStatementType.sstselect; 549 gnewsqlstatementtype = ESqlStatementType.sstselect; 550 return ret; 551 } 552 553 // DuckDB: CTE followed by FROM-first syntax (WITH ... FROM t) 554 if ((vendor == EDbVendor.dbvduckdb) && (lctoken.tokencode == TBaseType.rrw_from)) { 555 ret = new TSelectSqlStatement(vendor); 556 ret.isctequery = true; 557 gnewsqlstatementtype = ESqlStatementType.sstselect; 558 return ret; 559 } 560 561 return null; 562 } 563 564 /** 565 * Walk over the whole CTE list of a WITH clause and return the first solid 566 * token of the main query that follows it. Leading parentheses of a 567 * parenthesized main query — WITH c AS (...) (SELECT ...) — are skipped so 568 * the caller sees the statement keyword itself. 569 * 570 * <p>The walk only accepts the standard CTE-list shape 571 * <code>[RECURSIVE] name [(col, ...)] AS [[NOT] MATERIALIZED] ( ... ) [, ...]</code>. 572 * It returns null for anything else (XMLNAMESPACES, Oracle SEARCH/CYCLE 573 * clauses, malformed input) so the caller can fall back to the legacy 574 * keyword scan instead of guessing. 575 * 576 * @param withToken the WITH token that opens the clause 577 * @return first solid token of the main query, or null when the CTE list 578 * does not have the expected shape 579 */ 580 private TSourceToken findMainQueryTokenAfterCteList(TSourceToken withToken) { 581 TSourceToken token = withToken.nextSolidToken(); 582 if (token == null) return null; 583 584 // non-identifier-compare: RECURSIVE is a keyword that has no dedicated 585 // token code in every dialect, so it is matched by its text. 586 if ("RECURSIVE".equalsIgnoreCase(token.toString())) { 587 token = token.nextSolidToken(); 588 if (token == null) return null; 589 } 590 591 while (true) { 592 // CTE name 593 if (!isPlainNameToken(token)) return null; 594 token = token.nextSolidToken(); 595 if (token == null) return null; 596 597 // optional column alias list 598 if (token.tokentype == ETokenType.ttleftparenthesis) { 599 token = skipParenthesizedGroup(token); 600 if (token == null) return null; 601 } 602 603 if (token.tokencode != TBaseType.rrw_as) return null; 604 token = token.nextSolidToken(); 605 if (token == null) return null; 606 607 // optional [NOT] MATERIALIZED hint between AS and the CTE body 608 while (token.tokentype != ETokenType.ttleftparenthesis) { 609 // non-identifier-compare: materialization hint keywords 610 String text = token.toString(); 611 if (!"NOT".equalsIgnoreCase(text) && !"MATERIALIZED".equalsIgnoreCase(text)) return null; 612 token = token.nextSolidToken(); 613 if (token == null) return null; 614 } 615 616 // CTE body 617 token = skipParenthesizedGroup(token); 618 if (token == null) return null; 619 620 if (token.tokencode != ',') break; 621 622 token = token.nextSolidToken(); 623 if (token == null) return null; 624 } 625 626 // The main query may be wrapped in its own parentheses. 627 while (token.tokentype == ETokenType.ttleftparenthesis) { 628 token = token.nextSolidToken(); 629 if (token == null) return null; 630 } 631 632 return token; 633 } 634 635 /** 636 * @return true when the token can be a CTE name 637 */ 638 private boolean isPlainNameToken(TSourceToken token) { 639 if (token.tokentype == ETokenType.ttleftparenthesis) return false; 640 if (token.tokentype == ETokenType.ttrightparenthesis) return false; 641 if (token.tokentype == ETokenType.ttsemicolon) return false; 642 if (token.tokencode == ',') return false; 643 return true; 644 } 645 646 /** 647 * Skip a balanced parenthesized group. 648 * 649 * @param openToken the '(' token that opens the group 650 * @return the first solid token after the matching ')', or null when the 651 * parentheses are unbalanced or nothing follows the group 652 */ 653 private TSourceToken skipParenthesizedGroup(TSourceToken openToken) { 654 TSourceTokenList list = openToken.container; 655 int nested = 0; 656 for (int i = openToken.posinlist; i < list.size(); i++) { 657 TSourceToken t = list.get(i); 658 if (t.isnonsolidtoken()) continue; 659 if (t.tokentype == ETokenType.ttleftparenthesis) nested++; 660 else if (t.tokentype == ETokenType.ttrightparenthesis) { 661 nested--; 662 if (nested == 0) return t.nextSolidToken(); 663 if (nested < 0) return null; 664 } 665 } 666 return null; 667 } 668}