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