View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.blocks;
21  
22  import java.util.Arrays;
23  import java.util.Locale;
24  import java.util.Optional;
25  
26  import com.puppycrawl.tools.checkstyle.StatelessCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
32  
33  /**
34   * <div>
35   * Checks the placement of right curly braces (<code>'}'</code>) for code blocks. This check
36   * supports if-else, try-catch-finally blocks, switch statements, switch cases, while-loops,
37   *  for-loops, method definitions, class definitions, constructor definitions,
38   * instance, static initialization blocks, annotation definitions and enum definitions.
39   * For right curly brace of expression blocks of arrays, lambdas and class instances
40   * please follow issue
41   * <a href="https://github.com/checkstyle/checkstyle/issues/5945">#5945</a>.
42   * For right curly brace of enum constant please follow issue
43   * <a href="https://github.com/checkstyle/checkstyle/issues/7519">#7519</a>.
44   * </div>
45   *
46   * <ul>
47   * <li>
48   * Property {@code option} - Specify the policy on placement of a right curly brace
49   * (<code>'}'</code>).
50   * Type is {@code com.puppycrawl.tools.checkstyle.checks.blocks.RightCurlyOption}.
51   * Default value is {@code same}.
52   * </li>
53   * <li>
54   * Property {@code tokens} - tokens to check
55   * Type is {@code java.lang.String[]}.
56   * Validation type is {@code tokenSet}.
57   * Default value is:
58   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_TRY">
59   * LITERAL_TRY</a>,
60   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_CATCH">
61   * LITERAL_CATCH</a>,
62   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FINALLY">
63   * LITERAL_FINALLY</a>,
64   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF">
65   * LITERAL_IF</a>,
66   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_ELSE">
67   * LITERAL_ELSE</a>.
68   * </li>
69   * </ul>
70   *
71   * <p>
72   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
73   * </p>
74   *
75   * <p>
76   * Violation Message Keys:
77   * </p>
78   * <ul>
79   * <li>
80   * {@code line.alone}
81   * </li>
82   * <li>
83   * {@code line.break.before}
84   * </li>
85   * <li>
86   * {@code line.same}
87   * </li>
88   * </ul>
89   *
90   * @since 3.0
91   */
92  @StatelessCheck
93  public class RightCurlyCheck extends AbstractCheck {
94  
95      /**
96       * A key is pointing to the warning message text in "messages.properties"
97       * file.
98       */
99      public static final String MSG_KEY_LINE_BREAK_BEFORE = "line.break.before";
100 
101     /**
102      * A key is pointing to the warning message text in "messages.properties"
103      * file.
104      */
105     public static final String MSG_KEY_LINE_ALONE = "line.alone";
106 
107     /**
108      * A key is pointing to the warning message text in "messages.properties"
109      * file.
110      */
111     public static final String MSG_KEY_LINE_SAME = "line.same";
112 
113     /**
114      * Specify the policy on placement of a right curly brace (<code>'}'</code>).
115      */
116     private RightCurlyOption option = RightCurlyOption.SAME;
117 
118     /**
119      * Setter to specify the policy on placement of a right curly brace (<code>'}'</code>).
120      *
121      * @param optionStr string to decode option from
122      * @throws IllegalArgumentException if unable to decode
123      * @since 3.0
124      */
125     public void setOption(String optionStr) {
126         option = RightCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
127     }
128 
129     @Override
130     public int[] getDefaultTokens() {
131         return new int[] {
132             TokenTypes.LITERAL_TRY,
133             TokenTypes.LITERAL_CATCH,
134             TokenTypes.LITERAL_FINALLY,
135             TokenTypes.LITERAL_IF,
136             TokenTypes.LITERAL_ELSE,
137         };
138     }
139 
140     @Override
141     public int[] getAcceptableTokens() {
142         return new int[] {
143             TokenTypes.LITERAL_TRY,
144             TokenTypes.LITERAL_CATCH,
145             TokenTypes.LITERAL_FINALLY,
146             TokenTypes.LITERAL_IF,
147             TokenTypes.LITERAL_ELSE,
148             TokenTypes.CLASS_DEF,
149             TokenTypes.METHOD_DEF,
150             TokenTypes.CTOR_DEF,
151             TokenTypes.LITERAL_FOR,
152             TokenTypes.LITERAL_WHILE,
153             TokenTypes.LITERAL_DO,
154             TokenTypes.STATIC_INIT,
155             TokenTypes.INSTANCE_INIT,
156             TokenTypes.ANNOTATION_DEF,
157             TokenTypes.ENUM_DEF,
158             TokenTypes.INTERFACE_DEF,
159             TokenTypes.RECORD_DEF,
160             TokenTypes.COMPACT_CTOR_DEF,
161             TokenTypes.LITERAL_SWITCH,
162             TokenTypes.LITERAL_CASE,
163         };
164     }
165 
166     @Override
167     public int[] getRequiredTokens() {
168         return CommonUtil.EMPTY_INT_ARRAY;
169     }
170 
171     @Override
172     public void visitToken(DetailAST ast) {
173         final Details details = Details.getDetails(ast);
174         final DetailAST rcurly = details.rcurly;
175 
176         if (rcurly != null) {
177             final String violation = validate(details);
178             if (!violation.isEmpty()) {
179                 log(rcurly, violation, "}", rcurly.getColumnNo() + 1);
180             }
181         }
182     }
183 
184     /**
185      * Does general validation.
186      *
187      * @param details for validation.
188      * @return violation message or empty string
189      *     if there was no violation during validation.
190      */
191     private String validate(Details details) {
192         String violation = "";
193         if (shouldHaveLineBreakBefore(option, details)) {
194             violation = MSG_KEY_LINE_BREAK_BEFORE;
195         }
196         else if (shouldBeOnSameLine(option, details)) {
197             violation = MSG_KEY_LINE_SAME;
198         }
199         else if (shouldBeAloneOnLine(option, details, getLine(details.rcurly.getLineNo() - 1))) {
200             violation = MSG_KEY_LINE_ALONE;
201         }
202         return violation;
203     }
204 
205     /**
206      * Checks whether a right curly should have a line break before.
207      *
208      * @param bracePolicy option for placing the right curly brace.
209      * @param details details for validation.
210      * @return true if a right curly should have a line break before.
211      */
212     private static boolean shouldHaveLineBreakBefore(RightCurlyOption bracePolicy,
213                                                      Details details) {
214         return bracePolicy == RightCurlyOption.SAME
215                 && !hasLineBreakBefore(details.rcurly)
216                 && !TokenUtil.areOnSameLine(details.lcurly, details.rcurly);
217     }
218 
219     /**
220      * Checks that a right curly should be on the same line as the next statement.
221      *
222      * @param bracePolicy option for placing the right curly brace
223      * @param details Details for validation
224      * @return true if a right curly should be alone on a line.
225      */
226     private static boolean shouldBeOnSameLine(RightCurlyOption bracePolicy, Details details) {
227         return bracePolicy == RightCurlyOption.SAME
228                 && !details.shouldCheckLastRcurly
229                 && !TokenUtil.areOnSameLine(details.rcurly, details.nextToken);
230     }
231 
232     /**
233      * Checks that a right curly should be alone on a line.
234      *
235      * @param bracePolicy option for placing the right curly brace
236      * @param details Details for validation
237      * @param targetSrcLine A string with contents of rcurly's line
238      * @return true if a right curly should be alone on a line.
239      */
240     private static boolean shouldBeAloneOnLine(RightCurlyOption bracePolicy,
241                                                Details details,
242                                                String targetSrcLine) {
243         return bracePolicy == RightCurlyOption.ALONE
244                     && shouldBeAloneOnLineWithAloneOption(details, targetSrcLine)
245                 || (bracePolicy == RightCurlyOption.ALONE_OR_SINGLELINE
246                     || details.shouldCheckLastRcurly)
247                     && shouldBeAloneOnLineWithNotAloneOption(details, targetSrcLine);
248     }
249 
250     /**
251      * Whether right curly should be alone on line when ALONE option is used.
252      *
253      * @param details details for validation.
254      * @param targetSrcLine A string with contents of rcurly's line
255      * @return true, if right curly should be alone on line when ALONE option is used.
256      */
257     private static boolean shouldBeAloneOnLineWithAloneOption(Details details,
258                                                               String targetSrcLine) {
259         return !isAloneOnLine(details, targetSrcLine);
260     }
261 
262     /**
263      * Whether right curly should be alone on line when ALONE_OR_SINGLELINE or SAME option is used.
264      *
265      * @param details details for validation.
266      * @param targetSrcLine A string with contents of rcurly's line
267      * @return true, if right curly should be alone on line
268      *         when ALONE_OR_SINGLELINE or SAME option is used.
269      */
270     private static boolean shouldBeAloneOnLineWithNotAloneOption(Details details,
271                                                                  String targetSrcLine) {
272         return shouldBeAloneOnLineWithAloneOption(details, targetSrcLine)
273                 && !isBlockAloneOnSingleLine(details);
274     }
275 
276     /**
277      * Checks whether right curly is alone on a line.
278      *
279      * @param details for validation.
280      * @param targetSrcLine A string with contents of rcurly's line
281      * @return true if right curly is alone on a line.
282      */
283     private static boolean isAloneOnLine(Details details, String targetSrcLine) {
284         final DetailAST rcurly = details.rcurly;
285         final DetailAST nextToken = details.nextToken;
286         return (nextToken == null || !TokenUtil.areOnSameLine(rcurly, nextToken)
287             || skipDoubleBraceInstInit(details))
288             && CommonUtil.hasWhitespaceBefore(details.rcurly.getColumnNo(),
289                targetSrcLine);
290     }
291 
292     /**
293      * This method determines if the double brace initialization should be skipped over by the
294      * check. Double brace initializations are treated differently. The corresponding inner
295      * rcurly is treated as if it was alone on line even when it may be followed by another
296      * rcurly and a semi, raising no violations.
297      * <i>Please do note though that the line should not contain anything other than the following
298      * right curly and the semi following it or else violations will be raised.</i>
299      * Only the kind of double brace initializations shown in the following example code will be
300      * skipped over:<br>
301      * <pre>
302      *     {@code Map<String, String> map = new LinkedHashMap<>() {{
303      *           put("alpha", "man");
304      *       }}; // no violation}
305      * </pre>
306      *
307      * @param details {@link Details} object containing the details relevant to the rcurly
308      * @return if the double brace initialization rcurly should be skipped over by the check
309      */
310     private static boolean skipDoubleBraceInstInit(Details details) {
311         boolean skipDoubleBraceInstInit = false;
312         final DetailAST tokenAfterNextToken = Details.getNextToken(details.nextToken);
313         if (tokenAfterNextToken != null) {
314             final DetailAST rcurly = details.rcurly;
315             skipDoubleBraceInstInit = rcurly.getParent().getParent()
316                     .getType() == TokenTypes.INSTANCE_INIT
317                     && details.nextToken.getType() == TokenTypes.RCURLY
318                     && !TokenUtil.areOnSameLine(rcurly, Details.getNextToken(tokenAfterNextToken));
319         }
320         return skipDoubleBraceInstInit;
321     }
322 
323     /**
324      * Checks whether block has a single-line format and is alone on a line.
325      *
326      * @param details for validation.
327      * @return true if block has single-line format and is alone on a line.
328      */
329     private static boolean isBlockAloneOnSingleLine(Details details) {
330         DetailAST nextToken = details.nextToken;
331 
332         while (nextToken != null && nextToken.getType() == TokenTypes.LITERAL_ELSE) {
333             nextToken = Details.getNextToken(nextToken);
334         }
335 
336         // sibling tokens should be allowed on a single line
337         final int[] tokensWithBlockSibling = {
338             TokenTypes.DO_WHILE,
339             TokenTypes.LITERAL_FINALLY,
340             TokenTypes.LITERAL_CATCH,
341         };
342 
343         if (TokenUtil.isOfType(nextToken, tokensWithBlockSibling)) {
344             final DetailAST parent = nextToken.getParent();
345             nextToken = Details.getNextToken(parent);
346         }
347 
348         return TokenUtil.areOnSameLine(details.lcurly, details.rcurly)
349             && (nextToken == null || !TokenUtil.areOnSameLine(details.rcurly, nextToken)
350                 || isRightcurlyFollowedBySemicolon(details));
351     }
352 
353     /**
354      * Checks whether the right curly is followed by a semicolon.
355      *
356      * @param details details for validation.
357      * @return true if the right curly is followed by a semicolon.
358      */
359     private static boolean isRightcurlyFollowedBySemicolon(Details details) {
360         return details.nextToken.getType() == TokenTypes.SEMI;
361     }
362 
363     /**
364      * Checks if right curly has line break before.
365      *
366      * @param rightCurly right curly token.
367      * @return true, if right curly has line break before.
368      */
369     private static boolean hasLineBreakBefore(DetailAST rightCurly) {
370         DetailAST previousToken = rightCurly.getPreviousSibling();
371         if (previousToken == null) {
372             previousToken = rightCurly.getParent();
373         }
374         return !TokenUtil.areOnSameLine(rightCurly, previousToken);
375     }
376 
377     /**
378      * Structure that contains all details for validation.
379      */
380     private static final class Details {
381 
382         /**
383          * Token types that identify tokens that will never have SLIST in their AST.
384          */
385         private static final int[] TOKENS_WITH_NO_CHILD_SLIST = {
386             TokenTypes.CLASS_DEF,
387             TokenTypes.ENUM_DEF,
388             TokenTypes.ANNOTATION_DEF,
389             TokenTypes.INTERFACE_DEF,
390             TokenTypes.RECORD_DEF,
391         };
392 
393         /** Right curly. */
394         private final DetailAST rcurly;
395         /** Left curly. */
396         private final DetailAST lcurly;
397         /** Next token. */
398         private final DetailAST nextToken;
399         /** Should check last right curly. */
400         private final boolean shouldCheckLastRcurly;
401 
402         /**
403          * Constructor.
404          *
405          * @param lcurly the lcurly of the token whose details are being collected
406          * @param rcurly the rcurly of the token whose details are being collected
407          * @param nextToken the token after the token whose details are being collected
408          * @param shouldCheckLastRcurly boolean value to determine if to check last rcurly
409          */
410         private Details(DetailAST lcurly, DetailAST rcurly,
411                         DetailAST nextToken, boolean shouldCheckLastRcurly) {
412             this.lcurly = lcurly;
413             this.rcurly = rcurly;
414             this.nextToken = nextToken;
415             this.shouldCheckLastRcurly = shouldCheckLastRcurly;
416         }
417 
418         /**
419          * Collects validation Details.
420          *
421          * @param ast a {@code DetailAST} value
422          * @return object containing all details to make a validation
423          */
424         private static Details getDetails(DetailAST ast) {
425             final Details details;
426             switch (ast.getType()) {
427                 case TokenTypes.LITERAL_TRY:
428                 case TokenTypes.LITERAL_CATCH:
429                     details = getDetailsForTryCatch(ast);
430                     break;
431                 case TokenTypes.LITERAL_IF:
432                     details = getDetailsForIf(ast);
433                     break;
434                 case TokenTypes.LITERAL_DO:
435                     details = getDetailsForDoLoops(ast);
436                     break;
437                 case TokenTypes.LITERAL_SWITCH:
438                     details = getDetailsForSwitch(ast);
439                     break;
440                 case TokenTypes.LITERAL_CASE:
441                     details = getDetailsForCase(ast);
442                     break;
443                 default:
444                     details = getDetailsForOthers(ast);
445                     break;
446             }
447             return details;
448         }
449 
450         /**
451          * Collects details about switch statements and expressions.
452          *
453          * @param switchNode switch statement or expression to gather details about
454          * @return new Details about given switch statement or expression
455          */
456         private static Details getDetailsForSwitch(DetailAST switchNode) {
457             final DetailAST lcurly = switchNode.findFirstToken(TokenTypes.LCURLY);
458             final DetailAST rcurly;
459             DetailAST nextToken = null;
460             // skipping switch expression as check only handles statements
461             if (isSwitchExpression(switchNode)) {
462                 rcurly = null;
463             }
464             else {
465                 rcurly = switchNode.getLastChild();
466                 nextToken = getNextToken(switchNode);
467             }
468             return new Details(lcurly, rcurly, nextToken, true);
469         }
470 
471         /**
472          * Collects details about case statements.
473          *
474          * @param caseNode case statement to gather details about
475          * @return new Details about given case statement
476          */
477         private static Details getDetailsForCase(DetailAST caseNode) {
478             final DetailAST caseParent = caseNode.getParent();
479             final int parentType = caseParent.getType();
480             final Optional<DetailAST> lcurly;
481             final DetailAST statementList;
482 
483             if (parentType == TokenTypes.SWITCH_RULE) {
484                 statementList = caseParent.findFirstToken(TokenTypes.SLIST);
485                 lcurly = Optional.ofNullable(statementList);
486             }
487             else {
488                 statementList = caseNode.getNextSibling();
489                 lcurly = Optional.ofNullable(statementList)
490                          .map(DetailAST::getFirstChild)
491                          .filter(node -> node.getType() == TokenTypes.SLIST);
492             }
493             final DetailAST rcurly = lcurly.map(DetailAST::getLastChild)
494                     .filter(child -> !isSwitchExpression(caseParent))
495                     .orElse(null);
496             final Optional<DetailAST> nextToken =
497                     Optional.ofNullable(lcurly.map(DetailAST::getNextSibling)
498                     .orElseGet(() -> getNextToken(caseParent)));
499 
500             return new Details(lcurly.orElse(null), rcurly, nextToken.orElse(null), true);
501         }
502 
503         /**
504          * Check whether switch is expression or not.
505          *
506          * @param switchNode switch statement or expression to provide detail
507          * @return true if it is a switch expression
508          */
509         private static boolean isSwitchExpression(DetailAST switchNode) {
510             DetailAST currentNode = switchNode;
511             boolean ans = false;
512 
513             while (currentNode != null) {
514                 if (currentNode.getType() == TokenTypes.EXPR) {
515                     ans = true;
516                 }
517                 currentNode = currentNode.getParent();
518             }
519             return ans;
520         }
521 
522         /**
523          * Collects validation details for LITERAL_TRY, and LITERAL_CATCH.
524          *
525          * @param ast a {@code DetailAST} value
526          * @return object containing all details to make a validation
527          */
528         private static Details getDetailsForTryCatch(DetailAST ast) {
529             final DetailAST lcurly;
530             DetailAST nextToken;
531             final int tokenType = ast.getType();
532             if (tokenType == TokenTypes.LITERAL_TRY) {
533                 if (ast.getFirstChild().getType() == TokenTypes.RESOURCE_SPECIFICATION) {
534                     lcurly = ast.getFirstChild().getNextSibling();
535                 }
536                 else {
537                     lcurly = ast.getFirstChild();
538                 }
539                 nextToken = lcurly.getNextSibling();
540             }
541             else {
542                 nextToken = ast.getNextSibling();
543                 lcurly = ast.getLastChild();
544             }
545 
546             final boolean shouldCheckLastRcurly;
547             if (nextToken == null) {
548                 shouldCheckLastRcurly = true;
549                 nextToken = getNextToken(ast);
550             }
551             else {
552                 shouldCheckLastRcurly = false;
553             }
554 
555             final DetailAST rcurly = lcurly.getLastChild();
556             return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly);
557         }
558 
559         /**
560          * Collects validation details for LITERAL_IF.
561          *
562          * @param ast a {@code DetailAST} value
563          * @return object containing all details to make a validation
564          */
565         private static Details getDetailsForIf(DetailAST ast) {
566             final boolean shouldCheckLastRcurly;
567             final DetailAST lcurly;
568             DetailAST nextToken = ast.findFirstToken(TokenTypes.LITERAL_ELSE);
569 
570             if (nextToken == null) {
571                 shouldCheckLastRcurly = true;
572                 nextToken = getNextToken(ast);
573                 lcurly = ast.getLastChild();
574             }
575             else {
576                 shouldCheckLastRcurly = false;
577                 lcurly = nextToken.getPreviousSibling();
578             }
579 
580             DetailAST rcurly = null;
581             if (lcurly.getType() == TokenTypes.SLIST) {
582                 rcurly = lcurly.getLastChild();
583             }
584             return new Details(lcurly, rcurly, nextToken, shouldCheckLastRcurly);
585         }
586 
587         /**
588          * Collects validation details for CLASS_DEF, RECORD_DEF, METHOD DEF, CTOR_DEF, STATIC_INIT,
589          * INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, and COMPACT_CTOR_DEF.
590          *
591          * @param ast a {@code DetailAST} value
592          * @return an object containing all details to make a validation
593          */
594         private static Details getDetailsForOthers(DetailAST ast) {
595             DetailAST rcurly = null;
596             final DetailAST lcurly;
597             final int tokenType = ast.getType();
598             if (isTokenWithNoChildSlist(tokenType)) {
599                 final DetailAST child = ast.getLastChild();
600                 lcurly = child;
601                 rcurly = child.getLastChild();
602             }
603             else {
604                 lcurly = ast.findFirstToken(TokenTypes.SLIST);
605                 if (lcurly != null) {
606                     // SLIST could be absent if method is abstract
607                     rcurly = lcurly.getLastChild();
608                 }
609             }
610             return new Details(lcurly, rcurly, getNextToken(ast), true);
611         }
612 
613         /**
614          * Tests whether the provided tokenType will never have a SLIST as child in its AST.
615          * Like CLASS_DEF, ANNOTATION_DEF etc.
616          *
617          * @param tokenType the tokenType to test against.
618          * @return weather provided tokenType is definition token.
619          */
620         private static boolean isTokenWithNoChildSlist(int tokenType) {
621             return Arrays.stream(TOKENS_WITH_NO_CHILD_SLIST).anyMatch(token -> token == tokenType);
622         }
623 
624         /**
625          * Collects validation details for LITERAL_DO loops' tokens.
626          *
627          * @param ast a {@code DetailAST} value
628          * @return an object containing all details to make a validation
629          */
630         private static Details getDetailsForDoLoops(DetailAST ast) {
631             final DetailAST lcurly = ast.findFirstToken(TokenTypes.SLIST);
632             final DetailAST nextToken = ast.findFirstToken(TokenTypes.DO_WHILE);
633             DetailAST rcurly = null;
634             if (lcurly != null) {
635                 rcurly = lcurly.getLastChild();
636             }
637             return new Details(lcurly, rcurly, nextToken, false);
638         }
639 
640         /**
641          * Finds next token after the given one.
642          *
643          * @param ast the given node.
644          * @return the token which represents next lexical item.
645          */
646         private static DetailAST getNextToken(DetailAST ast) {
647             DetailAST next = null;
648             DetailAST parent = ast;
649             while (next == null && parent != null) {
650                 next = parent.getNextSibling();
651                 parent = parent.getParent();
652             }
653             return next;
654         }
655     }
656 }