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