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