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