View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.indentation;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  import java.util.Locale;
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   * Controls the indentation between comments and surrounding code.
36   * Comments are indented at the same level as the surrounding code.
37   * Detailed info about such convention can be found
38   * <a href="https://checkstyle.org/styleguides/google-java-style-20250426/javaguide.html#s4.8.6.1-block-comment-style">
39   * here</a>
40   * </div>
41   *
42   * @since 6.10
43   */
44  @StatelessCheck
45  public class CommentsIndentationCheck extends AbstractCheck {
46  
47      /**
48       * A key is pointing to the warning message text in "messages.properties" file.
49       */
50      public static final String MSG_KEY_SINGLE = "comments.indentation.single";
51  
52      /**
53       * A key is pointing to the warning message text in "messages.properties" file.
54       */
55      public static final String MSG_KEY_BLOCK = "comments.indentation.block";
56  
57      @Override
58      public int[] getDefaultTokens() {
59          return new int[] {
60              TokenTypes.SINGLE_LINE_COMMENT,
61              TokenTypes.BLOCK_COMMENT_BEGIN,
62          };
63      }
64  
65      @Override
66      public int[] getAcceptableTokens() {
67          return new int[] {
68              TokenTypes.SINGLE_LINE_COMMENT,
69              TokenTypes.BLOCK_COMMENT_BEGIN,
70          };
71      }
72  
73      @Override
74      public int[] getRequiredTokens() {
75          return CommonUtil.EMPTY_INT_ARRAY;
76      }
77  
78      @Override
79      public boolean isCommentNodesRequired() {
80          return true;
81      }
82  
83      @Override
84      public void visitToken(DetailAST commentAst) {
85          switch (commentAst.getType()) {
86              case TokenTypes.SINGLE_LINE_COMMENT, TokenTypes.BLOCK_COMMENT_BEGIN ->
87                  visitComment(commentAst);
88  
89              default -> {
90                  final String exceptionMsg = "Unexpected token type: " + commentAst.getText();
91                  throw new IllegalArgumentException(exceptionMsg);
92              }
93          }
94      }
95  
96      /**
97       * Checks comment indentations over surrounding code, e.g.:
98       *
99       * <p>
100      * {@code
101      * // some comment - this is ok
102      * double d = 3.14;
103      *     // some comment - this is <b>not</b> ok.
104      * double d1 = 5.0;
105      * }
106      * </p>
107      *
108      * @param comment comment to check.
109      */
110     private void visitComment(DetailAST comment) {
111         if (!isTrailingComment(comment)) {
112             final DetailAST prevStmt = getPreviousStatement(comment);
113             final DetailAST nextStmt = getNextStmt(comment);
114 
115             if (isInEmptyCaseBlock(prevStmt, nextStmt)) {
116                 handleCommentInEmptyCaseBlock(prevStmt, comment, nextStmt);
117             }
118             else if (isFallThroughComment(prevStmt, nextStmt)) {
119                 handleFallThroughComment(prevStmt, comment, nextStmt);
120             }
121             else if (isInEmptyCodeBlock(prevStmt, nextStmt)) {
122                 handleCommentInEmptyCodeBlock(comment, nextStmt);
123             }
124             else if (isCommentAtTheEndOfTheCodeBlock(nextStmt)) {
125                 handleCommentAtTheEndOfTheCodeBlock(prevStmt, comment, nextStmt);
126             }
127             else if (nextStmt != null && !areSameLevelIndented(comment, nextStmt, nextStmt)
128                     && !areInSameMethodCallWithSameIndent(comment)) {
129                 log(comment, getMessageKey(comment), nextStmt.getLineNo(),
130                     comment.getColumnNo(), nextStmt.getColumnNo());
131             }
132         }
133     }
134 
135     /**
136      * Returns the next statement of a comment.
137      *
138      * @param comment comment.
139      * @return the next statement of a comment.
140      */
141     private static DetailAST getNextStmt(DetailAST comment) {
142         DetailAST nextStmt = comment.getNextSibling();
143         while (nextStmt != null
144                 && isComment(nextStmt)
145                 && comment.getColumnNo() != nextStmt.getColumnNo()) {
146             nextStmt = nextStmt.getNextSibling();
147         }
148         return nextStmt;
149     }
150 
151     /**
152      * Returns the previous statement of a comment.
153      *
154      * @param comment comment.
155      * @return the previous statement of a comment.
156      */
157     private DetailAST getPreviousStatement(DetailAST comment) {
158         final DetailAST prevStatement;
159         if (isDistributedPreviousStatement(comment)) {
160             prevStatement = getDistributedPreviousStatement(comment);
161         }
162         else {
163             prevStatement = getOneLinePreviousStatement(comment);
164         }
165         return prevStatement;
166     }
167 
168     /**
169      * Checks whether the previous statement of a comment is distributed over two or more lines.
170      *
171      * @param comment comment to check.
172      * @return true if the previous statement of a comment is distributed over two or more lines.
173      */
174     private boolean isDistributedPreviousStatement(DetailAST comment) {
175         final DetailAST previousSibling = comment.getPreviousSibling();
176         return isDistributedExpression(comment)
177             || isDistributedReturnStatement(previousSibling)
178             || isDistributedThrowStatement(previousSibling);
179     }
180 
181     /**
182      * Checks whether the previous statement of a comment is a method call chain or
183      * string concatenation statement distributed over two or more lines.
184      *
185      * @param comment comment to check.
186      * @return true if the previous statement is a distributed expression.
187      */
188     private boolean isDistributedExpression(DetailAST comment) {
189         DetailAST previousSibling = comment.getPreviousSibling();
190         while (previousSibling != null && isComment(previousSibling)) {
191             previousSibling = previousSibling.getPreviousSibling();
192         }
193         boolean isDistributed = false;
194         if (previousSibling != null) {
195             if (previousSibling.getType() == TokenTypes.SEMI
196                     && isOnPreviousLineIgnoringComments(comment, previousSibling)) {
197                 DetailAST currentToken = previousSibling.getPreviousSibling();
198                 while (currentToken.getFirstChild() != null) {
199                     currentToken = currentToken.getFirstChild();
200                 }
201                 if (!TokenUtil.areOnSameLine(previousSibling, currentToken)) {
202                     isDistributed = true;
203                 }
204             }
205             else {
206                 isDistributed = isStatementWithPossibleCurlies(previousSibling);
207             }
208         }
209         return isDistributed;
210     }
211 
212     /**
213      * Whether the statement can have or always have curly brackets.
214      *
215      * @param previousSibling the statement to check.
216      * @return true if the statement can have or always have curly brackets.
217      */
218     private static boolean isStatementWithPossibleCurlies(DetailAST previousSibling) {
219         return previousSibling.getType() == TokenTypes.LITERAL_IF
220             || previousSibling.getType() == TokenTypes.LITERAL_TRY
221             || previousSibling.getType() == TokenTypes.LITERAL_FOR
222             || previousSibling.getType() == TokenTypes.LITERAL_DO
223             || previousSibling.getType() == TokenTypes.LITERAL_WHILE
224             || previousSibling.getType() == TokenTypes.LITERAL_SWITCH
225             || isDefinition(previousSibling);
226     }
227 
228     /**
229      * Whether the statement is a kind of definition (method, class etc.).
230      *
231      * @param previousSibling the statement to check.
232      * @return true if the statement is a kind of definition.
233      */
234     private static boolean isDefinition(DetailAST previousSibling) {
235         return TokenUtil.isTypeDeclaration(previousSibling.getType())
236             || previousSibling.getType() == TokenTypes.METHOD_DEF;
237     }
238 
239     /**
240      * Checks whether the previous statement of a comment is a distributed return statement.
241      *
242      * @param commentPreviousSibling previous sibling of the comment.
243      * @return true if the previous statement of a comment is a distributed return statement.
244      */
245     private static boolean isDistributedReturnStatement(DetailAST commentPreviousSibling) {
246         boolean isDistributed = false;
247         if (commentPreviousSibling != null
248                 && commentPreviousSibling.getType() == TokenTypes.LITERAL_RETURN) {
249             final DetailAST firstChild = commentPreviousSibling.getFirstChild();
250             final DetailAST nextSibling = firstChild.getNextSibling();
251             if (nextSibling != null) {
252                 isDistributed = true;
253             }
254         }
255         return isDistributed;
256     }
257 
258     /**
259      * Checks whether the previous statement of a comment is a distributed throw statement.
260      *
261      * @param commentPreviousSibling previous sibling of the comment.
262      * @return true if the previous statement of a comment is a distributed throw statement.
263      */
264     private static boolean isDistributedThrowStatement(DetailAST commentPreviousSibling) {
265         boolean isDistributed = false;
266         if (commentPreviousSibling != null
267                 && commentPreviousSibling.getType() == TokenTypes.LITERAL_THROW) {
268             final DetailAST firstChild = commentPreviousSibling.getFirstChild();
269             final DetailAST nextSibling = firstChild.getNextSibling();
270             if (!TokenUtil.areOnSameLine(nextSibling, commentPreviousSibling)) {
271                 isDistributed = true;
272             }
273         }
274         return isDistributed;
275     }
276 
277     /**
278      * Returns the first token of the distributed previous statement of comment.
279      *
280      * @param comment comment to check.
281      * @return the first token of the distributed previous statement of comment.
282      */
283     private static DetailAST getDistributedPreviousStatement(DetailAST comment) {
284         DetailAST currentToken = comment.getPreviousSibling();
285         while (isComment(currentToken)) {
286             currentToken = currentToken.getPreviousSibling();
287         }
288         final DetailAST previousStatement;
289         if (currentToken.getType() == TokenTypes.SEMI) {
290             currentToken = currentToken.getPreviousSibling();
291             while (currentToken.getFirstChild() != null) {
292                 if (isComment(currentToken)) {
293                     currentToken = currentToken.getNextSibling();
294                 }
295                 else {
296                     currentToken = currentToken.getFirstChild();
297                 }
298             }
299             previousStatement = currentToken;
300         }
301         else {
302             previousStatement = currentToken;
303         }
304         return previousStatement;
305     }
306 
307     /**
308      * Checks whether case block is empty.
309      *
310      * @param prevStmt next statement.
311      * @param nextStmt previous statement.
312      * @return true if case block is empty.
313      */
314     private static boolean isInEmptyCaseBlock(DetailAST prevStmt, DetailAST nextStmt) {
315         return prevStmt != null
316             && nextStmt != null
317             && (prevStmt.getType() == TokenTypes.LITERAL_CASE
318                 || prevStmt.getType() == TokenTypes.CASE_GROUP)
319             && (nextStmt.getType() == TokenTypes.LITERAL_CASE
320                 || nextStmt.getType() == TokenTypes.LITERAL_DEFAULT);
321     }
322 
323     /**
324      * Checks whether comment is a 'fall through' comment.
325      * For example:
326      *
327      * <p>
328      * {@code
329      *    ...
330      *    case OPTION_ONE:
331      *        int someVariable = 1;
332      *        // fall through
333      *    case OPTION_TWO:
334      *        int a = 5;
335      *        break;
336      *    ...
337      * }
338      * </p>
339      *
340      * @param prevStmt previous statement.
341      * @param nextStmt next statement.
342      * @return true if a comment is a 'fall through' comment.
343      */
344     private static boolean isFallThroughComment(DetailAST prevStmt, DetailAST nextStmt) {
345         return prevStmt != null
346             && nextStmt != null
347             && prevStmt.getType() != TokenTypes.LITERAL_CASE
348             && (nextStmt.getType() == TokenTypes.LITERAL_CASE
349                 || nextStmt.getType() == TokenTypes.LITERAL_DEFAULT);
350     }
351 
352     /**
353      * Checks whether a comment is placed at the end of the code block.
354      *
355      * @param nextStmt next statement.
356      * @return true if a comment is placed at the end of the block.
357      */
358     private static boolean isCommentAtTheEndOfTheCodeBlock(DetailAST nextStmt) {
359         return nextStmt != null
360             && nextStmt.getType() == TokenTypes.RCURLY;
361     }
362 
363     /**
364      * Checks whether comment is placed in the empty code block.
365      * For example:
366      *
367      * <p>
368      * ...
369      * {@code
370      *  // empty code block
371      * }
372      * ...
373      * </p>
374      * Note, the method does not treat empty case blocks.
375      *
376      * @param prevStmt previous statement.
377      * @param nextStmt next statement.
378      * @return true if comment is placed in the empty code block.
379      */
380     private static boolean isInEmptyCodeBlock(DetailAST prevStmt, DetailAST nextStmt) {
381         return prevStmt != null
382             && nextStmt != null
383             && (prevStmt.getType() == TokenTypes.SLIST
384                 || prevStmt.getType() == TokenTypes.LCURLY
385                 || prevStmt.getType() == TokenTypes.ARRAY_INIT
386                 || prevStmt.getType() == TokenTypes.OBJBLOCK)
387             && nextStmt.getType() == TokenTypes.RCURLY;
388     }
389 
390     /**
391      * Handles a comment which is placed within empty case block.
392      * Note, if comment is placed at the end of the empty case block, we have Checkstyle's
393      * limitations to clearly detect user intention of explanation target - above or below. The
394      * only case we can assume as a violation is when a single-line comment within the empty case
395      * block has indentation level that is lower than the indentation level of the next case
396      * token. For example:
397      *
398      * <p>
399      * {@code
400      *    ...
401      *    case OPTION_ONE:
402      * // violation
403      *    case OPTION_TWO:
404      *    ...
405      * }
406      * </p>
407      *
408      * @param prevStmt previous statement.
409      * @param comment single-line comment.
410      * @param nextStmt next statement.
411      */
412     private void handleCommentInEmptyCaseBlock(DetailAST prevStmt, DetailAST comment,
413                                                DetailAST nextStmt) {
414         if (comment.getColumnNo() < prevStmt.getColumnNo()
415                 || comment.getColumnNo() < nextStmt.getColumnNo()) {
416             logMultilineIndentation(prevStmt, comment, nextStmt);
417         }
418     }
419 
420     /**
421      * Handles 'fall through' single-line comment.
422      * Note, 'fall through' and similar comments can have indentation level as next or previous
423      * statement.
424      * For example:
425      *
426      * <p>
427      * {@code
428      *    ...
429      *    case OPTION_ONE:
430      *        int someVariable = 1;
431      *        // fall through - OK
432      *    case OPTION_TWO:
433      *        int a = 5;
434      *        break;
435      *    ...
436      * }
437      * </p>
438      *
439      * <p>
440      * {@code
441      *    ...
442      *    case OPTION_ONE:
443      *        int someVariable = 1;
444      *    // then init variable a - OK
445      *    case OPTION_TWO:
446      *        int a = 5;
447      *        break;
448      *    ...
449      * }
450      * </p>
451      *
452      * @param prevStmt previous statement.
453      * @param comment single-line comment.
454      * @param nextStmt next statement.
455      */
456     private void handleFallThroughComment(DetailAST prevStmt, DetailAST comment,
457                                           DetailAST nextStmt) {
458         if (!areSameLevelIndented(comment, prevStmt, nextStmt)) {
459             logMultilineIndentation(prevStmt, comment, nextStmt);
460         }
461     }
462 
463     /**
464      * Handles a comment which is placed at the end of non-empty code block.
465      * Note, if single-line comment is placed at the end of non-empty block the comment should have
466      * the same indentation level as the previous statement. For example:
467      *
468      * <p>
469      * {@code
470      *    if (a == true) {
471      *        int b = 1;
472      *        // comment
473      *    }
474      * }
475      * </p>
476      *
477      * @param prevStmt previous statement.
478      * @param comment comment to check.
479      * @param nextStmt next statement.
480      */
481     private void handleCommentAtTheEndOfTheCodeBlock(DetailAST prevStmt, DetailAST comment,
482                                                      DetailAST nextStmt) {
483         if (prevStmt != null) {
484             if (prevStmt.getType() == TokenTypes.LITERAL_CASE
485                     || prevStmt.getType() == TokenTypes.CASE_GROUP
486                     || prevStmt.getType() == TokenTypes.LITERAL_DEFAULT) {
487                 if (comment.getColumnNo() < nextStmt.getColumnNo()) {
488                     log(comment, getMessageKey(comment), nextStmt.getLineNo(),
489                         comment.getColumnNo(), nextStmt.getColumnNo());
490                 }
491             }
492             else if (isCommentForMultiblock(nextStmt)) {
493                 if (!areSameLevelIndented(comment, prevStmt, nextStmt)) {
494                     logMultilineIndentation(prevStmt, comment, nextStmt);
495                 }
496             }
497             else if (!areSameLevelIndented(comment, prevStmt, prevStmt)) {
498                 final int prevStmtLineNo = prevStmt.getLineNo();
499                 log(comment, getMessageKey(comment), prevStmtLineNo,
500                         comment.getColumnNo(), getLineStart(prevStmtLineNo));
501             }
502         }
503     }
504 
505     /**
506      * Whether the comment might have been used for the next block in a multi-block structure.
507      *
508      * @param endBlockStmt the end of the current block.
509      * @return true, if the comment might have been used for the next
510      *     block in a multi-block structure.
511      */
512     private static boolean isCommentForMultiblock(DetailAST endBlockStmt) {
513         final DetailAST nextBlock = endBlockStmt.getParent().getNextSibling();
514         final int endBlockLineNo = endBlockStmt.getLineNo();
515         final DetailAST catchAst = endBlockStmt.getParent().getParent();
516         final DetailAST finallyAst = catchAst.getNextSibling();
517         return nextBlock != null && nextBlock.getLineNo() == endBlockLineNo
518                 || finallyAst != null
519                     && catchAst.getType() == TokenTypes.LITERAL_CATCH
520                     && finallyAst.getLineNo() == endBlockLineNo;
521     }
522 
523     /**
524      * Handles a comment which is placed within the empty code block.
525      * Note, if comment is placed at the end of the empty code block, we have Checkstyle's
526      * limitations to clearly detect user intention of explanation target - above or below. The
527      * only case we can assume as a violation is when a single-line comment within the empty
528      * code block has indentation level that is lower than the indentation level of the closing
529      * right curly brace. For example:
530      *
531      * <p>
532      * {@code
533      *    if (a == true) {
534      * // violation
535      *    }
536      * }
537      * </p>
538      *
539      * @param comment comment to check.
540      * @param nextStmt next statement.
541      */
542     private void handleCommentInEmptyCodeBlock(DetailAST comment, DetailAST nextStmt) {
543         if (comment.getColumnNo() < nextStmt.getColumnNo()) {
544             log(comment, getMessageKey(comment), nextStmt.getLineNo(),
545                 comment.getColumnNo(), nextStmt.getColumnNo());
546         }
547     }
548 
549     /**
550      * Does pre-order traverse of abstract syntax tree to find the previous statement of the
551      * comment. If previous statement of the comment is found, then the traverse will
552      * be finished.
553      *
554      * @param comment current statement.
555      * @return previous statement of the comment or null if the comment does not have previous
556      *         statement.
557      */
558     private DetailAST getOneLinePreviousStatement(DetailAST comment) {
559         DetailAST root = comment.getParent();
560         while (root != null && !isBlockStart(root)) {
561             root = root.getParent();
562         }
563 
564         final Deque<DetailAST> stack = new ArrayDeque<>();
565         DetailAST previousStatement = null;
566         while (root != null || !stack.isEmpty()) {
567             if (!stack.isEmpty()) {
568                 root = stack.pop();
569             }
570             while (root != null) {
571                 previousStatement = findPreviousStatement(comment, root);
572                 if (previousStatement != null) {
573                     root = null;
574                     stack.clear();
575                     break;
576                 }
577                 if (root.getNextSibling() != null) {
578                     stack.push(root.getNextSibling());
579                 }
580                 root = root.getFirstChild();
581             }
582         }
583         return previousStatement;
584     }
585 
586     /**
587      * Whether the ast is a comment.
588      *
589      * @param ast the ast to check.
590      * @return true if the ast is a comment.
591      */
592     private static boolean isComment(DetailAST ast) {
593         final int astType = ast.getType();
594         return astType == TokenTypes.SINGLE_LINE_COMMENT
595             || astType == TokenTypes.BLOCK_COMMENT_BEGIN
596             || astType == TokenTypes.COMMENT_CONTENT
597             || astType == TokenTypes.BLOCK_COMMENT_END;
598     }
599 
600     /**
601      * Whether the AST node starts a block.
602      *
603      * @param root the AST node to check.
604      * @return true if the AST node starts a block.
605      */
606     private static boolean isBlockStart(DetailAST root) {
607         return root.getType() == TokenTypes.SLIST
608                 || root.getType() == TokenTypes.OBJBLOCK
609                 || root.getType() == TokenTypes.ARRAY_INIT
610                 || root.getType() == TokenTypes.CASE_GROUP;
611     }
612 
613     /**
614      * Finds a previous statement of the comment.
615      * Uses root token of the line while searching.
616      *
617      * @param comment comment.
618      * @param root root token of the line.
619      * @return previous statement of the comment or null if previous statement was not found.
620      */
621     private DetailAST findPreviousStatement(DetailAST comment, DetailAST root) {
622         DetailAST previousStatement = null;
623         if (Math.max(root.getLineNo(), comment.getLineNo()) == root.getLineNo()) {
624             // ATTENTION: parent of the comment is below the comment in case block
625             // See https://github.com/checkstyle/checkstyle/issues/851
626             previousStatement = getPrevStatementFromSwitchBlock(comment);
627         }
628         final DetailAST tokenWhichBeginsTheLine;
629         if (root.getType() == TokenTypes.EXPR) {
630             tokenWhichBeginsTheLine = findStartTokenOfMethodCallChain(root);
631         }
632         else if (root.getType() == TokenTypes.PLUS) {
633             tokenWhichBeginsTheLine = root.getFirstChild();
634         }
635         else {
636             tokenWhichBeginsTheLine = root;
637         }
638         if (tokenWhichBeginsTheLine != null
639                 && !isComment(tokenWhichBeginsTheLine)
640                 && isOnPreviousLineIgnoringComments(comment, tokenWhichBeginsTheLine)) {
641             previousStatement = tokenWhichBeginsTheLine;
642         }
643         return previousStatement;
644     }
645 
646     /**
647      * Finds the start token of method call chain.
648      *
649      * @param root root token of the line.
650      * @return the start token of method call chain.
651      */
652     private static DetailAST findStartTokenOfMethodCallChain(DetailAST root) {
653         DetailAST startOfMethodCallChain = root;
654         while (startOfMethodCallChain.getFirstChild() != null
655                 && TokenUtil.areOnSameLine(startOfMethodCallChain.getFirstChild(), root)) {
656             startOfMethodCallChain = startOfMethodCallChain.getFirstChild();
657         }
658         if (startOfMethodCallChain.getFirstChild() != null) {
659             startOfMethodCallChain = startOfMethodCallChain.getFirstChild().getNextSibling();
660         }
661         return startOfMethodCallChain;
662     }
663 
664     /**
665      * Checks whether the checked statement is on the previous line ignoring empty lines
666      * and lines which contain only comments.
667      *
668      * @param currentStatement current statement.
669      * @param checkedStatement checked statement.
670      * @return true if checked statement is on the line which is previous to current statement
671      *     ignoring empty lines and lines which contain only comments.
672      */
673     private boolean isOnPreviousLineIgnoringComments(DetailAST currentStatement,
674                                                      DetailAST checkedStatement) {
675         DetailAST nextToken = getNextToken(checkedStatement);
676         int distanceAim = 1;
677         if (nextToken != null && isComment(nextToken)) {
678             distanceAim += countEmptyLines(checkedStatement, currentStatement);
679         }
680 
681         while (nextToken != null && nextToken != currentStatement && isComment(nextToken)) {
682             if (nextToken.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
683                 distanceAim += nextToken.getLastChild().getLineNo() - nextToken.getLineNo();
684             }
685             distanceAim++;
686             nextToken = nextToken.getNextSibling();
687         }
688         return currentStatement.getLineNo() - checkedStatement.getLineNo() == distanceAim;
689     }
690 
691     /**
692      * Get the token to start counting the number of lines to add to the distance aim from.
693      *
694      * @param checkedStatement the checked statement.
695      * @return the token to start counting the number of lines to add to the distance aim from.
696      */
697     private DetailAST getNextToken(DetailAST checkedStatement) {
698         DetailAST nextToken;
699         if (checkedStatement.getType() == TokenTypes.SLIST
700                 || checkedStatement.getType() == TokenTypes.ARRAY_INIT
701                 || checkedStatement.getType() == TokenTypes.CASE_GROUP) {
702             nextToken = checkedStatement.getFirstChild();
703         }
704         else {
705             nextToken = checkedStatement.getNextSibling();
706         }
707         if (nextToken != null && isComment(nextToken) && isTrailingComment(nextToken)) {
708             nextToken = nextToken.getNextSibling();
709         }
710         return nextToken;
711     }
712 
713     /**
714      * Count the number of empty lines between statements.
715      *
716      * @param startStatement start statement.
717      * @param endStatement end statement.
718      * @return the number of empty lines between statements.
719      */
720     private int countEmptyLines(DetailAST startStatement, DetailAST endStatement) {
721         int emptyLinesNumber = 0;
722         final String[] lines = getLines();
723         final int endLineNo = endStatement.getLineNo();
724         for (int lineNo = startStatement.getLineNo(); lineNo < endLineNo; lineNo++) {
725             if (CommonUtil.isBlank(lines[lineNo])) {
726                 emptyLinesNumber++;
727             }
728         }
729         return emptyLinesNumber;
730     }
731 
732     /**
733      * Logs comment which can have the same indentation level as next or previous statement.
734      *
735      * @param prevStmt previous statement.
736      * @param comment comment.
737      * @param nextStmt next statement.
738      */
739     private void logMultilineIndentation(DetailAST prevStmt, DetailAST comment,
740                                          DetailAST nextStmt) {
741         final String multilineNoTemplate = "%d, %d";
742         log(comment, getMessageKey(comment),
743             String.format(Locale.getDefault(), multilineNoTemplate, prevStmt.getLineNo(),
744                 nextStmt.getLineNo()), comment.getColumnNo(),
745             String.format(Locale.getDefault(), multilineNoTemplate,
746                     getLineStart(prevStmt.getLineNo()), getLineStart(nextStmt.getLineNo())));
747     }
748 
749     /**
750      * Get a message key depending on a comment type.
751      *
752      * @param comment the comment to process.
753      * @return a message key.
754      */
755     private static String getMessageKey(DetailAST comment) {
756         final String msgKey;
757         if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
758             msgKey = MSG_KEY_SINGLE;
759         }
760         else {
761             msgKey = MSG_KEY_BLOCK;
762         }
763         return msgKey;
764     }
765 
766     /**
767      * Gets comment's previous statement from switch block.
768      *
769      * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}.
770      * @return comment's previous statement or null if previous statement is absent.
771      */
772     private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) {
773         final DetailAST prevStmt;
774         final DetailAST parentStatement = comment.getParent();
775         if (parentStatement.getType() == TokenTypes.CASE_GROUP) {
776             prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement);
777         }
778         else {
779             prevStmt = getPrevCaseToken(parentStatement);
780         }
781         return prevStmt;
782     }
783 
784     /**
785      * Gets previous statement for comment which is placed immediately under case.
786      *
787      * @param parentStatement comment's parent statement.
788      * @return comment's previous statement or null if previous statement is absent.
789      */
790     private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) {
791         DetailAST prevStmt = null;
792         final DetailAST prevBlock = parentStatement.getPreviousSibling();
793         if (prevBlock.getLastChild() != null) {
794             DetailAST blockBody = prevBlock.getLastChild().getLastChild();
795             if (blockBody.getType() == TokenTypes.SEMI) {
796                 blockBody = blockBody.getPreviousSibling();
797             }
798             if (blockBody.getType() == TokenTypes.EXPR) {
799                 prevStmt = findStartTokenOfMethodCallChain(blockBody);
800             }
801             else {
802                 if (blockBody.getType() == TokenTypes.SLIST) {
803                     prevStmt = blockBody.getParent().getParent();
804                 }
805                 else {
806                     prevStmt = blockBody;
807                 }
808             }
809             if (isComment(prevStmt)) {
810                 prevStmt = prevStmt.getNextSibling();
811             }
812         }
813         return prevStmt;
814     }
815 
816     /**
817      * Gets previous case-token for comment.
818      *
819      * @param parentStatement comment's parent statement.
820      * @return previous case-token or null if previous case-token is absent.
821      */
822     private static DetailAST getPrevCaseToken(DetailAST parentStatement) {
823         final DetailAST prevCaseToken;
824         final DetailAST parentBlock = parentStatement.getParent();
825         if (parentBlock.getParent().getPreviousSibling() != null
826                 && parentBlock.getParent().getPreviousSibling().getType()
827                     == TokenTypes.LITERAL_CASE) {
828             prevCaseToken = parentBlock.getParent().getPreviousSibling();
829         }
830         else {
831             prevCaseToken = null;
832         }
833         return prevCaseToken;
834     }
835 
836     /**
837      * Checks if comment and next code statement
838      * (or previous code stmt like <b>case</b> in switch block) are indented at the same level,
839      * e.g.:
840      * <pre>
841      * {@code
842      * // some comment - same indentation level
843      * int x = 10;
844      *     // some comment - different indentation level
845      * int x1 = 5;
846      * /*
847      *  *
848      *  *&#47;
849      *  boolean bool = true; - same indentation level
850      * }
851      * </pre>
852      *
853      * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}.
854      * @param prevStmt previous code statement.
855      * @param nextStmt next code statement.
856      * @return true if comment and next code statement are indented at the same level.
857      */
858     private boolean areSameLevelIndented(DetailAST comment, DetailAST prevStmt,
859                                                 DetailAST nextStmt) {
860         return comment.getColumnNo() == getLineStart(nextStmt.getLineNo())
861             || comment.getColumnNo() == getLineStart(prevStmt.getLineNo());
862     }
863 
864     /**
865      * Get a column number where a code starts.
866      *
867      * @param lineNo the line number to get column number in.
868      * @return the column number where a code starts.
869      */
870     private int getLineStart(int lineNo) {
871         final char[] line = getLines()[lineNo - 1].toCharArray();
872         int lineStart = 0;
873         while (Character.isWhitespace(line[lineStart])) {
874             lineStart++;
875         }
876         return lineStart;
877     }
878 
879     /**
880      * Checks if current comment is a trailing comment.
881      *
882      * @param comment comment to check.
883      * @return true if current comment is a trailing comment.
884      */
885     private boolean isTrailingComment(DetailAST comment) {
886         final boolean isTrailingComment;
887         if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
888             isTrailingComment = isTrailingSingleLineComment(comment);
889         }
890         else {
891             isTrailingComment = isTrailingBlockComment(comment);
892         }
893         return isTrailingComment;
894     }
895 
896     /**
897      * Checks if current single-line comment is trailing comment, e.g.:
898      *
899      * <p>
900      * {@code
901      * double d = 3.14; // some comment
902      * }
903      * </p>
904      *
905      * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}.
906      * @return true if current single-line comment is trailing comment.
907      */
908     private boolean isTrailingSingleLineComment(DetailAST singleLineComment) {
909         final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1);
910         final int commentColumnNo = singleLineComment.getColumnNo();
911         return !CommonUtil.hasWhitespaceBefore(commentColumnNo, targetSourceLine);
912     }
913 
914     /**
915      * Checks if current comment block is trailing comment, e.g.:
916      *
917      * <p>
918      * {@code
919      * double d = 3.14; /* some comment *&#47;
920      * /* some comment *&#47; double d = 18.5;
921      * }
922      * </p>
923      *
924      * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}.
925      * @return true if current comment block is trailing comment.
926      */
927     private boolean isTrailingBlockComment(DetailAST blockComment) {
928         final String commentLine = getLine(blockComment.getLineNo() - 1);
929         final int commentColumnNo = blockComment.getColumnNo();
930         final DetailAST nextSibling = blockComment.getNextSibling();
931         return !CommonUtil.hasWhitespaceBefore(commentColumnNo, commentLine)
932             || nextSibling != null && TokenUtil.areOnSameLine(nextSibling, blockComment);
933     }
934 
935     /**
936      * Checks if the comment is inside a method call with same indentation of
937      * first expression. e.g:
938      *
939      * <p>
940      * {@code
941      * private final boolean myList = someMethod(
942      *     // Some comment here
943      *     s1,
944      *     s2,
945      *     s3
946      *     // ok
947      * );
948      * }
949      * </p>
950      *
951      * @param comment comment to check.
952      * @return true, if comment is inside a method call with same indentation.
953      */
954     private static boolean areInSameMethodCallWithSameIndent(DetailAST comment) {
955         return comment.getParent().getType() == TokenTypes.METHOD_CALL
956                 && comment.getColumnNo()
957                      == getFirstExpressionNodeFromMethodCall(comment.getParent()).getColumnNo();
958     }
959 
960     /**
961      * Returns the first EXPR DetailAST child from parent of comment.
962      *
963      * @param methodCall methodCall DetailAst from which node to be extracted.
964      * @return first EXPR DetailAST child from parent of comment.
965      */
966     private static DetailAST getFirstExpressionNodeFromMethodCall(DetailAST methodCall) {
967         // Method call always has ELIST
968         return methodCall.findFirstToken(TokenTypes.ELIST);
969     }
970 
971 }