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}