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