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