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