1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.indentation;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  import java.util.Locale;
25  
26  import com.puppycrawl.tools.checkstyle.StatelessCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31  
32  /**
33   * This Check controls the indentation between comments and surrounding code.
34   * Comments are indented at the same level as the surrounding code.
35   * Detailed info about such convention can be found
36   * <a href=
37   * "https://checkstyle.org/styleguides/google-java-style-20170228.html#s4.8.6.1-block-comment-style">
38   * here</a>
39   * <p>
40   * Examples:
41   * </p>
42   * <p>
43   * To configure the Check:
44   * </p>
45   *
46   * <pre>
47   * {@code
48   * &lt;module name=&quot;CommentsIndentation&quot;/&gt;
49   * }
50   * {@code
51   * /*
52   *  * comment
53   *  * some comment
54   *  *&#47;
55   * boolean bool = true; - such comment indentation is ok
56   *    /*
57   *    * comment
58   *    * some comment
59   *     *&#47;
60   * double d = 3.14; - Block Comment has incorrect indentation level 7, expected 4.
61   * // some comment - comment is ok
62   * String str = "";
63   *     // some comment Comment has incorrect indentation level 8, expected 4.
64   * String str1 = "";
65   * }
66   * </pre>
67   *
68   */
69  @StatelessCheck
70  public class CommentsIndentationCheck extends AbstractCheck {
71  
72      /**
73       * A key is pointing to the warning message text in "messages.properties" file.
74       */
75      public static final String MSG_KEY_SINGLE = "comments.indentation.single";
76  
77      /**
78       * A key is pointing to the warning message text in "messages.properties" file.
79       */
80      public static final String MSG_KEY_BLOCK = "comments.indentation.block";
81  
82      @Override
83      public int[] getDefaultTokens() {
84          return new int[] {
85              TokenTypes.SINGLE_LINE_COMMENT,
86              TokenTypes.BLOCK_COMMENT_BEGIN,
87          };
88      }
89  
90      @Override
91      public int[] getAcceptableTokens() {
92          return new int[] {
93              TokenTypes.SINGLE_LINE_COMMENT,
94              TokenTypes.BLOCK_COMMENT_BEGIN,
95          };
96      }
97  
98      @Override
99      public int[] getRequiredTokens() {
100         return CommonUtil.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         if (comment.getColumnNo() < prevStmt.getColumnNo()
424                 || comment.getColumnNo() < nextStmt.getColumnNo()) {
425             logMultilineIndentation(prevStmt, comment, nextStmt);
426         }
427     }
428 
429     /**
430      * Handles 'fall through' single line comment.
431      * Note, 'fall through' and similar comments can have indentation level as next or previous
432      * statement.
433      * For example:
434      * <p>
435      * {@code
436      *    ...
437      *    case OPTION_ONE:
438      *        int someVariable = 1;
439      *        // fall through - OK
440      *    case OPTION_TWO:
441      *        int a = 5;
442      *        break;
443      *    ...
444      * }
445      * </p>
446      * <p>
447      * {@code
448      *    ...
449      *    case OPTION_ONE:
450      *        int someVariable = 1;
451      *    // then init variable a - OK
452      *    case OPTION_TWO:
453      *        int a = 5;
454      *        break;
455      *    ...
456      * }
457      * </p>
458      * @param prevStmt previous statement.
459      * @param comment single line comment.
460      * @param nextStmt next statement.
461      */
462     private void handleFallThroughComment(DetailAST prevStmt, DetailAST comment,
463                                           DetailAST nextStmt) {
464         if (!areSameLevelIndented(comment, prevStmt, nextStmt)) {
465             logMultilineIndentation(prevStmt, comment, nextStmt);
466         }
467     }
468 
469     /**
470      * Handles a comment which is placed at the end of non empty code block.
471      * Note, if single line comment is placed at the end of non empty block the comment should have
472      * the same indentation level as the previous statement. For example:
473      * <p>
474      * {@code
475      *    if (a == true) {
476      *        int b = 1;
477      *        // comment
478      *    }
479      * }
480      * </p>
481      * @param prevStmt previous statement.
482      * @param comment comment to check.
483      * @param nextStmt next statement.
484      */
485     private void handleCommentAtTheEndOfTheCodeBlock(DetailAST prevStmt, DetailAST comment,
486                                                      DetailAST nextStmt) {
487         if (prevStmt != null) {
488             if (prevStmt.getType() == TokenTypes.LITERAL_CASE
489                     || prevStmt.getType() == TokenTypes.CASE_GROUP
490                     || prevStmt.getType() == TokenTypes.LITERAL_DEFAULT) {
491                 if (comment.getColumnNo() < nextStmt.getColumnNo()) {
492                     log(comment.getLineNo(), getMessageKey(comment), nextStmt.getLineNo(),
493                         comment.getColumnNo(), nextStmt.getColumnNo());
494                 }
495             }
496             else if (isCommentForMultiblock(nextStmt)) {
497                 if (!areSameLevelIndented(comment, prevStmt, nextStmt)) {
498                     logMultilineIndentation(prevStmt, comment, nextStmt);
499                 }
500             }
501             else if (!areSameLevelIndented(comment, prevStmt, prevStmt)) {
502                 final int prevStmtLineNo = prevStmt.getLineNo();
503                 log(comment.getLineNo(), getMessageKey(comment), prevStmtLineNo,
504                         comment.getColumnNo(), getLineStart(prevStmtLineNo));
505             }
506         }
507     }
508 
509     /**
510      * Whether the comment might have been used for the next block in a multi-block structure.
511      * @param endBlockStmt the end of the current block.
512      * @return true, if the comment might have been used for the next
513      *     block in a multi-block structure.
514      */
515     private static boolean isCommentForMultiblock(DetailAST endBlockStmt) {
516         final DetailAST nextBlock = endBlockStmt.getParent().getNextSibling();
517         final int endBlockLineNo = endBlockStmt.getLineNo();
518         final DetailAST catchAst = endBlockStmt.getParent().getParent();
519         final DetailAST finallyAst = catchAst.getNextSibling();
520         return nextBlock != null && nextBlock.getLineNo() == endBlockLineNo
521                 || finallyAst != null
522                     && catchAst.getType() == TokenTypes.LITERAL_CATCH
523                     && finallyAst.getLineNo() == endBlockLineNo;
524     }
525 
526     /**
527      * Handles a comment which is placed within the empty code block.
528      * Note, if comment is placed at the end of the empty code block, we have Checkstyle's
529      * limitations to clearly detect user intention of explanation target - above or below. The
530      * only case we can assume as a violation is when a single line comment within the empty
531      * code block has indentation level that is lower than the indentation level of the closing
532      * right curly brace. For example:
533      * <p>
534      * {@code
535      *    if (a == true) {
536      * // violation
537      *    }
538      * }
539      * </p>
540      *
541      * @param comment comment to check.
542      * @param nextStmt next statement.
543      */
544     private void handleCommentInEmptyCodeBlock(DetailAST comment, DetailAST nextStmt) {
545         if (comment.getColumnNo() < nextStmt.getColumnNo()) {
546             log(comment.getLineNo(), getMessageKey(comment), nextStmt.getLineNo(),
547                 comment.getColumnNo(), nextStmt.getColumnNo());
548         }
549     }
550 
551     /**
552      * Does pre-order traverse of abstract syntax tree to find the previous statement of the
553      * comment. If previous statement of the comment is found, then the traverse will
554      * be finished.
555      * @param comment current statement.
556      * @return previous statement of the comment or null if the comment does not have previous
557      *         statement.
558      */
559     private DetailAST getOneLinePreviousStatement(DetailAST comment) {
560         DetailAST root = comment.getParent();
561         while (root != null && !isBlockStart(root)) {
562             root = root.getParent();
563         }
564 
565         final Deque<DetailAST> stack = new ArrayDeque<>();
566         DetailAST previousStatement = null;
567         while (root != null || !stack.isEmpty()) {
568             if (!stack.isEmpty()) {
569                 root = stack.pop();
570             }
571             while (root != null) {
572                 previousStatement = findPreviousStatement(comment, root);
573                 if (previousStatement != null) {
574                     root = null;
575                     stack.clear();
576                     break;
577                 }
578                 if (root.getNextSibling() != null) {
579                     stack.push(root.getNextSibling());
580                 }
581                 root = root.getFirstChild();
582             }
583         }
584         return previousStatement;
585     }
586 
587     /**
588      * Whether the ast is a comment.
589      * @param ast the ast to check.
590      * @return true if the ast is a comment.
591      */
592     private static boolean isComment(DetailAST ast) {
593         final int astType = ast.getType();
594         return astType == TokenTypes.SINGLE_LINE_COMMENT
595             || astType == TokenTypes.BLOCK_COMMENT_BEGIN
596             || astType == TokenTypes.COMMENT_CONTENT
597             || astType == TokenTypes.BLOCK_COMMENT_END;
598     }
599 
600     /**
601      * Whether the AST node starts a block.
602      * @param root the AST node to check.
603      * @return true if the AST node starts a block.
604      */
605     private static boolean isBlockStart(DetailAST root) {
606         return root.getType() == TokenTypes.SLIST
607                 || root.getType() == TokenTypes.OBJBLOCK
608                 || root.getType() == TokenTypes.ARRAY_INIT
609                 || root.getType() == TokenTypes.CASE_GROUP;
610     }
611 
612     /**
613      * Finds a previous statement of the comment.
614      * Uses root token of the line while searching.
615      * @param comment comment.
616      * @param root root token of the line.
617      * @return previous statement of the comment or null if previous statement was not found.
618      */
619     private DetailAST findPreviousStatement(DetailAST comment, DetailAST root) {
620         DetailAST previousStatement = null;
621         if (root.getLineNo() >= comment.getLineNo()) {
622             // ATTENTION: parent of the comment is below the comment in case block
623             // See https://github.com/checkstyle/checkstyle/issues/851
624             previousStatement = getPrevStatementFromSwitchBlock(comment);
625         }
626         final DetailAST tokenWhichBeginsTheLine;
627         if (root.getType() == TokenTypes.EXPR
628                 && root.getFirstChild().getFirstChild() != null) {
629             if (root.getFirstChild().getType() == TokenTypes.LITERAL_NEW) {
630                 tokenWhichBeginsTheLine = root.getFirstChild();
631             }
632             else {
633                 tokenWhichBeginsTheLine = findTokenWhichBeginsTheLine(root);
634             }
635         }
636         else if (root.getType() == TokenTypes.PLUS) {
637             tokenWhichBeginsTheLine = root.getFirstChild();
638         }
639         else {
640             tokenWhichBeginsTheLine = root;
641         }
642         if (tokenWhichBeginsTheLine != null
643                 && !isComment(tokenWhichBeginsTheLine)
644                 && isOnPreviousLineIgnoringComments(comment, tokenWhichBeginsTheLine)) {
645             previousStatement = tokenWhichBeginsTheLine;
646         }
647         return previousStatement;
648     }
649 
650     /**
651      * Finds a token which begins the line.
652      * @param root root token of the line.
653      * @return token which begins the line.
654      */
655     private static DetailAST findTokenWhichBeginsTheLine(DetailAST root) {
656         final DetailAST tokenWhichBeginsTheLine;
657         if (isUsingOfObjectReferenceToInvokeMethod(root)) {
658             tokenWhichBeginsTheLine = findStartTokenOfMethodCallChain(root);
659         }
660         else {
661             tokenWhichBeginsTheLine = root.getFirstChild().findFirstToken(TokenTypes.IDENT);
662         }
663         return tokenWhichBeginsTheLine;
664     }
665 
666     /**
667      * Checks whether there is a use of an object reference to invoke an object's method on line.
668      * @param root root token of the line.
669      * @return true if there is a use of an object reference to invoke an object's method on line.
670      */
671     private static boolean isUsingOfObjectReferenceToInvokeMethod(DetailAST root) {
672         return root.getFirstChild().getFirstChild().getFirstChild() != null
673             && root.getFirstChild().getFirstChild().getFirstChild().getNextSibling() != null;
674     }
675 
676     /**
677      * Finds the start token of method call chain.
678      * @param root root token of the line.
679      * @return the start token of method call chain.
680      */
681     private static DetailAST findStartTokenOfMethodCallChain(DetailAST root) {
682         DetailAST startOfMethodCallChain = root;
683         while (startOfMethodCallChain.getFirstChild() != null
684                 && startOfMethodCallChain.getFirstChild().getLineNo() == root.getLineNo()) {
685             startOfMethodCallChain = startOfMethodCallChain.getFirstChild();
686         }
687         if (startOfMethodCallChain.getFirstChild() != null) {
688             startOfMethodCallChain = startOfMethodCallChain.getFirstChild().getNextSibling();
689         }
690         return startOfMethodCallChain;
691     }
692 
693     /**
694      * Checks whether the checked statement is on the previous line ignoring empty lines
695      * and lines which contain only comments.
696      * @param currentStatement current statement.
697      * @param checkedStatement checked statement.
698      * @return true if checked statement is on the line which is previous to current statement
699      *     ignoring empty lines and lines which contain only comments.
700      */
701     private boolean isOnPreviousLineIgnoringComments(DetailAST currentStatement,
702                                                      DetailAST checkedStatement) {
703         DetailAST nextToken = getNextToken(checkedStatement);
704         int distanceAim = 1;
705         if (nextToken != null && isComment(nextToken)) {
706             distanceAim += countEmptyLines(checkedStatement, currentStatement);
707         }
708 
709         while (nextToken != null && nextToken != currentStatement && isComment(nextToken)) {
710             if (nextToken.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
711                 distanceAim += nextToken.getLastChild().getLineNo() - nextToken.getLineNo();
712             }
713             distanceAim++;
714             nextToken = nextToken.getNextSibling();
715         }
716         return currentStatement.getLineNo() - checkedStatement.getLineNo() == distanceAim;
717     }
718 
719     /**
720      * Get the token to start counting the number of lines to add to the distance aim from.
721      * @param checkedStatement the checked statement.
722      * @return the token to start counting the number of lines to add to the distance aim from.
723      */
724     private DetailAST getNextToken(DetailAST checkedStatement) {
725         DetailAST nextToken;
726         if (checkedStatement.getType() == TokenTypes.SLIST
727                 || checkedStatement.getType() == TokenTypes.ARRAY_INIT
728                 || checkedStatement.getType() == TokenTypes.CASE_GROUP) {
729             nextToken = checkedStatement.getFirstChild();
730         }
731         else {
732             nextToken = checkedStatement.getNextSibling();
733         }
734         if (nextToken != null && isComment(nextToken) && isTrailingComment(nextToken)) {
735             nextToken = nextToken.getNextSibling();
736         }
737         return nextToken;
738     }
739 
740     /**
741      * Count the number of empty lines between statements.
742      * @param startStatement start statement.
743      * @param endStatement end statement.
744      * @return the number of empty lines between statements.
745      */
746     private int countEmptyLines(DetailAST startStatement, DetailAST endStatement) {
747         int emptyLinesNumber = 0;
748         final String[] lines = getLines();
749         final int endLineNo = endStatement.getLineNo();
750         for (int lineNo = startStatement.getLineNo(); lineNo < endLineNo; lineNo++) {
751             if (CommonUtil.isBlank(lines[lineNo])) {
752                 emptyLinesNumber++;
753             }
754         }
755         return emptyLinesNumber;
756     }
757 
758     /**
759      * Logs comment which can have the same indentation level as next or previous statement.
760      * @param comment comment.
761      * @param nextStmt next statement.
762      * @param prevStmt previous statement.
763      */
764     private void logMultilineIndentation(DetailAST prevStmt, DetailAST comment,
765                                          DetailAST nextStmt) {
766         final String multilineNoTemplate = "%d, %d";
767         log(comment.getLineNo(), getMessageKey(comment),
768             String.format(Locale.getDefault(), multilineNoTemplate, prevStmt.getLineNo(),
769                 nextStmt.getLineNo()), comment.getColumnNo(),
770             String.format(Locale.getDefault(), multilineNoTemplate,
771                     getLineStart(prevStmt.getLineNo()), getLineStart(nextStmt.getLineNo())));
772     }
773 
774     /**
775      * Get a message key depending on a comment type.
776      * @param comment the comment to process.
777      * @return a message key.
778      */
779     private static String getMessageKey(DetailAST comment) {
780         final String msgKey;
781         if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
782             msgKey = MSG_KEY_SINGLE;
783         }
784         else {
785             msgKey = MSG_KEY_BLOCK;
786         }
787         return msgKey;
788     }
789 
790     /**
791      * Gets comment's previous statement from switch block.
792      * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}.
793      * @return comment's previous statement or null if previous statement is absent.
794      */
795     private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) {
796         final DetailAST prevStmt;
797         final DetailAST parentStatement = comment.getParent();
798         if (parentStatement.getType() == TokenTypes.CASE_GROUP) {
799             prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement);
800         }
801         else {
802             prevStmt = getPrevCaseToken(parentStatement);
803         }
804         return prevStmt;
805     }
806 
807     /**
808      * Gets previous statement for comment which is placed immediately under case.
809      * @param parentStatement comment's parent statement.
810      * @return comment's previous statement or null if previous statement is absent.
811      */
812     private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) {
813         DetailAST prevStmt = null;
814         final DetailAST prevBlock = parentStatement.getPreviousSibling();
815         if (prevBlock.getLastChild() != null) {
816             DetailAST blockBody = prevBlock.getLastChild().getLastChild();
817             if (blockBody.getType() == TokenTypes.SEMI) {
818                 blockBody = blockBody.getPreviousSibling();
819             }
820             if (blockBody.getType() == TokenTypes.EXPR) {
821                 if (isUsingOfObjectReferenceToInvokeMethod(blockBody)) {
822                     prevStmt = findStartTokenOfMethodCallChain(blockBody);
823                 }
824                 else {
825                     prevStmt = blockBody.getFirstChild().getFirstChild();
826                 }
827             }
828             else {
829                 if (blockBody.getType() == TokenTypes.SLIST) {
830                     prevStmt = blockBody.getParent().getParent();
831                 }
832                 else {
833                     prevStmt = blockBody;
834                 }
835             }
836             if (isComment(prevStmt)) {
837                 prevStmt = prevStmt.getNextSibling();
838             }
839         }
840         return prevStmt;
841     }
842 
843     /**
844      * Gets previous case-token for comment.
845      * @param parentStatement comment's parent statement.
846      * @return previous case-token or null if previous case-token is absent.
847      */
848     private static DetailAST getPrevCaseToken(DetailAST parentStatement) {
849         final DetailAST prevCaseToken;
850         final DetailAST parentBlock = parentStatement.getParent();
851         if (parentBlock.getParent() != null
852                 && parentBlock.getParent().getPreviousSibling() != null
853                 && parentBlock.getParent().getPreviousSibling().getType()
854                     == TokenTypes.LITERAL_CASE) {
855             prevCaseToken = parentBlock.getParent().getPreviousSibling();
856         }
857         else {
858             prevCaseToken = null;
859         }
860         return prevCaseToken;
861     }
862 
863     /**
864      * Checks if comment and next code statement
865      * (or previous code stmt like <b>case</b> in switch block) are indented at the same level,
866      * e.g.:
867      * <p>
868      * <pre>
869      * {@code
870      * // some comment - same indentation level
871      * int x = 10;
872      *     // some comment - different indentation level
873      * int x1 = 5;
874      * /*
875      *  *
876      *  *&#47;
877      *  boolean bool = true; - same indentation level
878      * }
879      * </pre>
880      * </p>
881      * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}.
882      * @param prevStmt previous code statement.
883      * @param nextStmt next code statement.
884      * @return true if comment and next code statement are indented at the same level.
885      */
886     private boolean areSameLevelIndented(DetailAST comment, DetailAST prevStmt,
887                                                 DetailAST nextStmt) {
888         return comment.getColumnNo() == getLineStart(nextStmt.getLineNo())
889             || comment.getColumnNo() == getLineStart(prevStmt.getLineNo());
890     }
891 
892     /**
893      * Get a column number where a code starts.
894      * @param lineNo the line number to get column number in.
895      * @return the column number where a code starts.
896      */
897     private int getLineStart(int lineNo) {
898         final char[] line = getLines()[lineNo - 1].toCharArray();
899         int lineStart = 0;
900         while (Character.isWhitespace(line[lineStart])) {
901             lineStart++;
902         }
903         return lineStart;
904     }
905 
906     /**
907      * Checks if current comment is a trailing comment.
908      * @param comment comment to check.
909      * @return true if current comment is a trailing comment.
910      */
911     private boolean isTrailingComment(DetailAST comment) {
912         final boolean isTrailingComment;
913         if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
914             isTrailingComment = isTrailingSingleLineComment(comment);
915         }
916         else {
917             isTrailingComment = isTrailingBlockComment(comment);
918         }
919         return isTrailingComment;
920     }
921 
922     /**
923      * Checks if current single line comment is trailing comment, e.g.:
924      * <p>
925      * {@code
926      * double d = 3.14; // some comment
927      * }
928      * </p>
929      * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}.
930      * @return true if current single line comment is trailing comment.
931      */
932     private boolean isTrailingSingleLineComment(DetailAST singleLineComment) {
933         final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1);
934         final int commentColumnNo = singleLineComment.getColumnNo();
935         return !CommonUtil.hasWhitespaceBefore(commentColumnNo, targetSourceLine);
936     }
937 
938     /**
939      * Checks if current comment block is trailing comment, e.g.:
940      * <p>
941      * {@code
942      * double d = 3.14; /* some comment *&#47;
943      * /* some comment *&#47; double d = 18.5;
944      * }
945      * </p>
946      * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}.
947      * @return true if current comment block is trailing comment.
948      */
949     private boolean isTrailingBlockComment(DetailAST blockComment) {
950         final String commentLine = getLine(blockComment.getLineNo() - 1);
951         final int commentColumnNo = blockComment.getColumnNo();
952         final DetailAST nextSibling = blockComment.getNextSibling();
953         return !CommonUtil.hasWhitespaceBefore(commentColumnNo, commentLine)
954             || nextSibling != null && nextSibling.getLineNo() == blockComment.getLineNo();
955     }
956 
957 }