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