001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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    @Override
058    public int[] getDefaultTokens() {
059        return new int[] {
060            TokenTypes.SINGLE_LINE_COMMENT,
061            TokenTypes.BLOCK_COMMENT_BEGIN,
062        };
063    }
064
065    @Override
066    public int[] getAcceptableTokens() {
067        return new int[] {
068            TokenTypes.SINGLE_LINE_COMMENT,
069            TokenTypes.BLOCK_COMMENT_BEGIN,
070        };
071    }
072
073    @Override
074    public int[] getRequiredTokens() {
075        return CommonUtil.EMPTY_INT_ARRAY;
076    }
077
078    @Override
079    public boolean isCommentNodesRequired() {
080        return true;
081    }
082
083    @Override
084    public void visitToken(DetailAST commentAst) {
085        switch (commentAst.getType()) {
086            case TokenTypes.SINGLE_LINE_COMMENT, TokenTypes.BLOCK_COMMENT_BEGIN ->
087                visitComment(commentAst);
088
089            default -> {
090                final String exceptionMsg = "Unexpected token type: " + commentAst.getText();
091                throw new IllegalArgumentException(exceptionMsg);
092            }
093        }
094    }
095
096    /**
097     * Checks comment indentations over surrounding code, e.g.:
098     *
099     * <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 (root.getLineNo() >= comment.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                && root.getFirstChild().getFirstChild() != null) {
631            if (root.getFirstChild().getType() == TokenTypes.LITERAL_NEW) {
632                tokenWhichBeginsTheLine = root.getFirstChild();
633            }
634            else {
635                tokenWhichBeginsTheLine = findTokenWhichBeginsTheLine(root);
636            }
637        }
638        else if (root.getType() == TokenTypes.PLUS) {
639            tokenWhichBeginsTheLine = root.getFirstChild();
640        }
641        else {
642            tokenWhichBeginsTheLine = root;
643        }
644        if (tokenWhichBeginsTheLine != null
645                && !isComment(tokenWhichBeginsTheLine)
646                && isOnPreviousLineIgnoringComments(comment, tokenWhichBeginsTheLine)) {
647            previousStatement = tokenWhichBeginsTheLine;
648        }
649        return previousStatement;
650    }
651
652    /**
653     * Finds a token which begins the line.
654     *
655     * @param root root token of the line.
656     * @return token which begins the line.
657     */
658    private static DetailAST findTokenWhichBeginsTheLine(DetailAST root) {
659        final DetailAST tokenWhichBeginsTheLine;
660        if (isUsingOfObjectReferenceToInvokeMethod(root)) {
661            tokenWhichBeginsTheLine = findStartTokenOfMethodCallChain(root);
662        }
663        else {
664            tokenWhichBeginsTheLine = root.getFirstChild().findFirstToken(TokenTypes.IDENT);
665        }
666        return tokenWhichBeginsTheLine;
667    }
668
669    /**
670     * Checks whether there is a use of an object reference to invoke an object's method on line.
671     *
672     * @param root root token of the line.
673     * @return true if there is a use of an object reference to invoke an object's method on line.
674     */
675    private static boolean isUsingOfObjectReferenceToInvokeMethod(DetailAST root) {
676        return root.getFirstChild().getFirstChild().getFirstChild() != null
677            && root.getFirstChild().getFirstChild().getFirstChild().getNextSibling() != null;
678    }
679
680    /**
681     * Finds the start token of method call chain.
682     *
683     * @param root root token of the line.
684     * @return the start token of method call chain.
685     */
686    private static DetailAST findStartTokenOfMethodCallChain(DetailAST root) {
687        DetailAST startOfMethodCallChain = root;
688        while (startOfMethodCallChain.getFirstChild() != null
689                && TokenUtil.areOnSameLine(startOfMethodCallChain.getFirstChild(), root)) {
690            startOfMethodCallChain = startOfMethodCallChain.getFirstChild();
691        }
692        if (startOfMethodCallChain.getFirstChild() != null) {
693            startOfMethodCallChain = startOfMethodCallChain.getFirstChild().getNextSibling();
694        }
695        return startOfMethodCallChain;
696    }
697
698    /**
699     * Checks whether the checked statement is on the previous line ignoring empty lines
700     * and lines which contain only comments.
701     *
702     * @param currentStatement current statement.
703     * @param checkedStatement checked statement.
704     * @return true if checked statement is on the line which is previous to current statement
705     *     ignoring empty lines and lines which contain only comments.
706     */
707    private boolean isOnPreviousLineIgnoringComments(DetailAST currentStatement,
708                                                     DetailAST checkedStatement) {
709        DetailAST nextToken = getNextToken(checkedStatement);
710        int distanceAim = 1;
711        if (nextToken != null && isComment(nextToken)) {
712            distanceAim += countEmptyLines(checkedStatement, currentStatement);
713        }
714
715        while (nextToken != null && nextToken != currentStatement && isComment(nextToken)) {
716            if (nextToken.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
717                distanceAim += nextToken.getLastChild().getLineNo() - nextToken.getLineNo();
718            }
719            distanceAim++;
720            nextToken = nextToken.getNextSibling();
721        }
722        return currentStatement.getLineNo() - checkedStatement.getLineNo() == distanceAim;
723    }
724
725    /**
726     * Get the token to start counting the number of lines to add to the distance aim from.
727     *
728     * @param checkedStatement the checked statement.
729     * @return the token to start counting the number of lines to add to the distance aim from.
730     */
731    private DetailAST getNextToken(DetailAST checkedStatement) {
732        DetailAST nextToken;
733        if (checkedStatement.getType() == TokenTypes.SLIST
734                || checkedStatement.getType() == TokenTypes.ARRAY_INIT
735                || checkedStatement.getType() == TokenTypes.CASE_GROUP) {
736            nextToken = checkedStatement.getFirstChild();
737        }
738        else {
739            nextToken = checkedStatement.getNextSibling();
740        }
741        if (nextToken != null && isComment(nextToken) && isTrailingComment(nextToken)) {
742            nextToken = nextToken.getNextSibling();
743        }
744        return nextToken;
745    }
746
747    /**
748     * Count the number of empty lines between statements.
749     *
750     * @param startStatement start statement.
751     * @param endStatement end statement.
752     * @return the number of empty lines between statements.
753     */
754    private int countEmptyLines(DetailAST startStatement, DetailAST endStatement) {
755        int emptyLinesNumber = 0;
756        final String[] lines = getLines();
757        final int endLineNo = endStatement.getLineNo();
758        for (int lineNo = startStatement.getLineNo(); lineNo < endLineNo; lineNo++) {
759            if (CommonUtil.isBlank(lines[lineNo])) {
760                emptyLinesNumber++;
761            }
762        }
763        return emptyLinesNumber;
764    }
765
766    /**
767     * Logs comment which can have the same indentation level as next or previous statement.
768     *
769     * @param prevStmt previous statement.
770     * @param comment comment.
771     * @param nextStmt next statement.
772     */
773    private void logMultilineIndentation(DetailAST prevStmt, DetailAST comment,
774                                         DetailAST nextStmt) {
775        final String multilineNoTemplate = "%d, %d";
776        log(comment, getMessageKey(comment),
777            String.format(Locale.getDefault(), multilineNoTemplate, prevStmt.getLineNo(),
778                nextStmt.getLineNo()), comment.getColumnNo(),
779            String.format(Locale.getDefault(), multilineNoTemplate,
780                    getLineStart(prevStmt.getLineNo()), getLineStart(nextStmt.getLineNo())));
781    }
782
783    /**
784     * Get a message key depending on a comment type.
785     *
786     * @param comment the comment to process.
787     * @return a message key.
788     */
789    private static String getMessageKey(DetailAST comment) {
790        final String msgKey;
791        if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
792            msgKey = MSG_KEY_SINGLE;
793        }
794        else {
795            msgKey = MSG_KEY_BLOCK;
796        }
797        return msgKey;
798    }
799
800    /**
801     * Gets comment's previous statement from switch block.
802     *
803     * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}.
804     * @return comment's previous statement or null if previous statement is absent.
805     */
806    private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) {
807        final DetailAST prevStmt;
808        final DetailAST parentStatement = comment.getParent();
809        if (parentStatement.getType() == TokenTypes.CASE_GROUP) {
810            prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement);
811        }
812        else {
813            prevStmt = getPrevCaseToken(parentStatement);
814        }
815        return prevStmt;
816    }
817
818    /**
819     * Gets previous statement for comment which is placed immediately under case.
820     *
821     * @param parentStatement comment's parent statement.
822     * @return comment's previous statement or null if previous statement is absent.
823     */
824    private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) {
825        DetailAST prevStmt = null;
826        final DetailAST prevBlock = parentStatement.getPreviousSibling();
827        if (prevBlock.getLastChild() != null) {
828            DetailAST blockBody = prevBlock.getLastChild().getLastChild();
829            if (blockBody.getType() == TokenTypes.SEMI) {
830                blockBody = blockBody.getPreviousSibling();
831            }
832            if (blockBody.getType() == TokenTypes.EXPR) {
833                if (isUsingOfObjectReferenceToInvokeMethod(blockBody)) {
834                    prevStmt = findStartTokenOfMethodCallChain(blockBody);
835                }
836                else {
837                    prevStmt = blockBody.getFirstChild().getFirstChild();
838                }
839            }
840            else {
841                if (blockBody.getType() == TokenTypes.SLIST) {
842                    prevStmt = blockBody.getParent().getParent();
843                }
844                else {
845                    prevStmt = blockBody;
846                }
847            }
848            if (isComment(prevStmt)) {
849                prevStmt = prevStmt.getNextSibling();
850            }
851        }
852        return prevStmt;
853    }
854
855    /**
856     * Gets previous case-token for comment.
857     *
858     * @param parentStatement comment's parent statement.
859     * @return previous case-token or null if previous case-token is absent.
860     */
861    private static DetailAST getPrevCaseToken(DetailAST parentStatement) {
862        final DetailAST prevCaseToken;
863        final DetailAST parentBlock = parentStatement.getParent();
864        if (parentBlock.getParent().getPreviousSibling() != null
865                && parentBlock.getParent().getPreviousSibling().getType()
866                    == TokenTypes.LITERAL_CASE) {
867            prevCaseToken = parentBlock.getParent().getPreviousSibling();
868        }
869        else {
870            prevCaseToken = null;
871        }
872        return prevCaseToken;
873    }
874
875    /**
876     * Checks if comment and next code statement
877     * (or previous code stmt like <b>case</b> in switch block) are indented at the same level,
878     * e.g.:
879     * <pre>
880     * {@code
881     * // some comment - same indentation level
882     * int x = 10;
883     *     // some comment - different indentation level
884     * int x1 = 5;
885     * /*
886     *  *
887     *  *&#47;
888     *  boolean bool = true; - same indentation level
889     * }
890     * </pre>
891     *
892     * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}.
893     * @param prevStmt previous code statement.
894     * @param nextStmt next code statement.
895     * @return true if comment and next code statement are indented at the same level.
896     */
897    private boolean areSameLevelIndented(DetailAST comment, DetailAST prevStmt,
898                                                DetailAST nextStmt) {
899        return comment.getColumnNo() == getLineStart(nextStmt.getLineNo())
900            || comment.getColumnNo() == getLineStart(prevStmt.getLineNo());
901    }
902
903    /**
904     * Get a column number where a code starts.
905     *
906     * @param lineNo the line number to get column number in.
907     * @return the column number where a code starts.
908     */
909    private int getLineStart(int lineNo) {
910        final char[] line = getLines()[lineNo - 1].toCharArray();
911        int lineStart = 0;
912        while (Character.isWhitespace(line[lineStart])) {
913            lineStart++;
914        }
915        return lineStart;
916    }
917
918    /**
919     * Checks if current comment is a trailing comment.
920     *
921     * @param comment comment to check.
922     * @return true if current comment is a trailing comment.
923     */
924    private boolean isTrailingComment(DetailAST comment) {
925        final boolean isTrailingComment;
926        if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
927            isTrailingComment = isTrailingSingleLineComment(comment);
928        }
929        else {
930            isTrailingComment = isTrailingBlockComment(comment);
931        }
932        return isTrailingComment;
933    }
934
935    /**
936     * Checks if current single-line comment is trailing comment, e.g.:
937     *
938     * <p>
939     * {@code
940     * double d = 3.14; // some comment
941     * }
942     * </p>
943     *
944     * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}.
945     * @return true if current single-line comment is trailing comment.
946     */
947    private boolean isTrailingSingleLineComment(DetailAST singleLineComment) {
948        final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1);
949        final int commentColumnNo = singleLineComment.getColumnNo();
950        return !CommonUtil.hasWhitespaceBefore(commentColumnNo, targetSourceLine);
951    }
952
953    /**
954     * Checks if current comment block is trailing comment, e.g.:
955     *
956     * <p>
957     * {@code
958     * double d = 3.14; /* some comment *&#47;
959     * /* some comment *&#47; double d = 18.5;
960     * }
961     * </p>
962     *
963     * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}.
964     * @return true if current comment block is trailing comment.
965     */
966    private boolean isTrailingBlockComment(DetailAST blockComment) {
967        final String commentLine = getLine(blockComment.getLineNo() - 1);
968        final int commentColumnNo = blockComment.getColumnNo();
969        final DetailAST nextSibling = blockComment.getNextSibling();
970        return !CommonUtil.hasWhitespaceBefore(commentColumnNo, commentLine)
971            || nextSibling != null && TokenUtil.areOnSameLine(nextSibling, blockComment);
972    }
973
974    /**
975     * Checks if the comment is inside a method call with same indentation of
976     * first expression. e.g:
977     *
978     * <p>
979     * {@code
980     * private final boolean myList = someMethod(
981     *     // Some comment here
982     *     s1,
983     *     s2,
984     *     s3
985     *     // ok
986     * );
987     * }
988     * </p>
989     *
990     * @param comment comment to check.
991     * @return true, if comment is inside a method call with same indentation.
992     */
993    private static boolean areInSameMethodCallWithSameIndent(DetailAST comment) {
994        return comment.getParent().getType() == TokenTypes.METHOD_CALL
995                && comment.getColumnNo()
996                     == getFirstExpressionNodeFromMethodCall(comment.getParent()).getColumnNo();
997    }
998
999    /**
1000     * Returns the first EXPR DetailAST child from parent of comment.
1001     *
1002     * @param methodCall methodCall DetailAst from which node to be extracted.
1003     * @return first EXPR DetailAST child from parent of comment.
1004     */
1005    private static DetailAST getFirstExpressionNodeFromMethodCall(DetailAST methodCall) {
1006        // Method call always has ELIST
1007        return methodCall.findFirstToken(TokenTypes.ELIST);
1008    }
1009
1010}