1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
35
36
37
38
39
40
41
42
43
44 @StatelessCheck
45 public class CommentsIndentationCheck extends AbstractCheck {
46
47
48
49
50 public static final String MSG_KEY_SINGLE = "comments.indentation.single";
51
52
53
54
55 public static final String MSG_KEY_BLOCK = "comments.indentation.block";
56
57 @Override
58 public int[] getDefaultTokens() {
59 return new int[] {
60 TokenTypes.SINGLE_LINE_COMMENT,
61 TokenTypes.BLOCK_COMMENT_BEGIN,
62 };
63 }
64
65 @Override
66 public int[] getAcceptableTokens() {
67 return new int[] {
68 TokenTypes.SINGLE_LINE_COMMENT,
69 TokenTypes.BLOCK_COMMENT_BEGIN,
70 };
71 }
72
73 @Override
74 public int[] getRequiredTokens() {
75 return CommonUtil.EMPTY_INT_ARRAY;
76 }
77
78 @Override
79 public boolean isCommentNodesRequired() {
80 return true;
81 }
82
83 @Override
84 public void visitToken(DetailAST commentAst) {
85 switch (commentAst.getType()) {
86 case TokenTypes.SINGLE_LINE_COMMENT, TokenTypes.BLOCK_COMMENT_BEGIN ->
87 visitComment(commentAst);
88
89 default -> {
90 final String exceptionMsg = "Unexpected token type: " + commentAst.getText();
91 throw new IllegalArgumentException(exceptionMsg);
92 }
93 }
94 }
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 private void visitComment(DetailAST comment) {
111 if (!isTrailingComment(comment)) {
112 final DetailAST prevStmt = getPreviousStatement(comment);
113 final DetailAST nextStmt = getNextStmt(comment);
114
115 if (isInEmptyCaseBlock(prevStmt, nextStmt)) {
116 handleCommentInEmptyCaseBlock(prevStmt, comment, nextStmt);
117 }
118 else if (isFallThroughComment(prevStmt, nextStmt)) {
119 handleFallThroughComment(prevStmt, comment, nextStmt);
120 }
121 else if (isInEmptyCodeBlock(prevStmt, nextStmt)) {
122 handleCommentInEmptyCodeBlock(comment, nextStmt);
123 }
124 else if (isCommentAtTheEndOfTheCodeBlock(nextStmt)) {
125 handleCommentAtTheEndOfTheCodeBlock(prevStmt, comment, nextStmt);
126 }
127 else if (nextStmt != null && !areSameLevelIndented(comment, nextStmt, nextStmt)
128 && !areInSameMethodCallWithSameIndent(comment)) {
129 log(comment, getMessageKey(comment), nextStmt.getLineNo(),
130 comment.getColumnNo(), nextStmt.getColumnNo());
131 }
132 }
133 }
134
135
136
137
138
139
140
141 private static DetailAST getNextStmt(DetailAST comment) {
142 DetailAST nextStmt = comment.getNextSibling();
143 while (nextStmt != null
144 && isComment(nextStmt)
145 && comment.getColumnNo() != nextStmt.getColumnNo()) {
146 nextStmt = nextStmt.getNextSibling();
147 }
148 return nextStmt;
149 }
150
151
152
153
154
155
156
157 private DetailAST getPreviousStatement(DetailAST comment) {
158 final DetailAST prevStatement;
159 if (isDistributedPreviousStatement(comment)) {
160 prevStatement = getDistributedPreviousStatement(comment);
161 }
162 else {
163 prevStatement = getOneLinePreviousStatement(comment);
164 }
165 return prevStatement;
166 }
167
168
169
170
171
172
173
174 private boolean isDistributedPreviousStatement(DetailAST comment) {
175 final DetailAST previousSibling = comment.getPreviousSibling();
176 return isDistributedExpression(comment)
177 || isDistributedReturnStatement(previousSibling)
178 || isDistributedThrowStatement(previousSibling);
179 }
180
181
182
183
184
185
186
187
188 private boolean isDistributedExpression(DetailAST comment) {
189 DetailAST previousSibling = comment.getPreviousSibling();
190 while (previousSibling != null && isComment(previousSibling)) {
191 previousSibling = previousSibling.getPreviousSibling();
192 }
193 boolean isDistributed = false;
194 if (previousSibling != null) {
195 if (previousSibling.getType() == TokenTypes.SEMI
196 && isOnPreviousLineIgnoringComments(comment, previousSibling)) {
197 DetailAST currentToken = previousSibling.getPreviousSibling();
198 while (currentToken.getFirstChild() != null) {
199 currentToken = currentToken.getFirstChild();
200 }
201 if (!TokenUtil.areOnSameLine(previousSibling, currentToken)) {
202 isDistributed = true;
203 }
204 }
205 else {
206 isDistributed = isStatementWithPossibleCurlies(previousSibling);
207 }
208 }
209 return isDistributed;
210 }
211
212
213
214
215
216
217
218 private static boolean isStatementWithPossibleCurlies(DetailAST previousSibling) {
219 return previousSibling.getType() == TokenTypes.LITERAL_IF
220 || previousSibling.getType() == TokenTypes.LITERAL_TRY
221 || previousSibling.getType() == TokenTypes.LITERAL_FOR
222 || previousSibling.getType() == TokenTypes.LITERAL_DO
223 || previousSibling.getType() == TokenTypes.LITERAL_WHILE
224 || previousSibling.getType() == TokenTypes.LITERAL_SWITCH
225 || isDefinition(previousSibling);
226 }
227
228
229
230
231
232
233
234 private static boolean isDefinition(DetailAST previousSibling) {
235 return TokenUtil.isTypeDeclaration(previousSibling.getType())
236 || previousSibling.getType() == TokenTypes.METHOD_DEF;
237 }
238
239
240
241
242
243
244
245 private static boolean isDistributedReturnStatement(DetailAST commentPreviousSibling) {
246 boolean isDistributed = false;
247 if (commentPreviousSibling != null
248 && commentPreviousSibling.getType() == TokenTypes.LITERAL_RETURN) {
249 final DetailAST firstChild = commentPreviousSibling.getFirstChild();
250 final DetailAST nextSibling = firstChild.getNextSibling();
251 if (nextSibling != null) {
252 isDistributed = true;
253 }
254 }
255 return isDistributed;
256 }
257
258
259
260
261
262
263
264 private static boolean isDistributedThrowStatement(DetailAST commentPreviousSibling) {
265 boolean isDistributed = false;
266 if (commentPreviousSibling != null
267 && commentPreviousSibling.getType() == TokenTypes.LITERAL_THROW) {
268 final DetailAST firstChild = commentPreviousSibling.getFirstChild();
269 final DetailAST nextSibling = firstChild.getNextSibling();
270 if (!TokenUtil.areOnSameLine(nextSibling, commentPreviousSibling)) {
271 isDistributed = true;
272 }
273 }
274 return isDistributed;
275 }
276
277
278
279
280
281
282
283 private static DetailAST getDistributedPreviousStatement(DetailAST comment) {
284 DetailAST currentToken = comment.getPreviousSibling();
285 while (isComment(currentToken)) {
286 currentToken = currentToken.getPreviousSibling();
287 }
288 final DetailAST previousStatement;
289 if (currentToken.getType() == TokenTypes.SEMI) {
290 currentToken = currentToken.getPreviousSibling();
291 while (currentToken.getFirstChild() != null) {
292 if (isComment(currentToken)) {
293 currentToken = currentToken.getNextSibling();
294 }
295 else {
296 currentToken = currentToken.getFirstChild();
297 }
298 }
299 previousStatement = currentToken;
300 }
301 else {
302 previousStatement = currentToken;
303 }
304 return previousStatement;
305 }
306
307
308
309
310
311
312
313
314 private static boolean isInEmptyCaseBlock(DetailAST prevStmt, DetailAST nextStmt) {
315 return prevStmt != null
316 && nextStmt != null
317 && (prevStmt.getType() == TokenTypes.LITERAL_CASE
318 || prevStmt.getType() == TokenTypes.CASE_GROUP)
319 && (nextStmt.getType() == TokenTypes.LITERAL_CASE
320 || nextStmt.getType() == TokenTypes.LITERAL_DEFAULT);
321 }
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344 private static boolean isFallThroughComment(DetailAST prevStmt, DetailAST nextStmt) {
345 return prevStmt != null
346 && nextStmt != null
347 && prevStmt.getType() != TokenTypes.LITERAL_CASE
348 && (nextStmt.getType() == TokenTypes.LITERAL_CASE
349 || nextStmt.getType() == TokenTypes.LITERAL_DEFAULT);
350 }
351
352
353
354
355
356
357
358 private static boolean isCommentAtTheEndOfTheCodeBlock(DetailAST nextStmt) {
359 return nextStmt != null
360 && nextStmt.getType() == TokenTypes.RCURLY;
361 }
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380 private static boolean isInEmptyCodeBlock(DetailAST prevStmt, DetailAST nextStmt) {
381 return prevStmt != null
382 && nextStmt != null
383 && (prevStmt.getType() == TokenTypes.SLIST
384 || prevStmt.getType() == TokenTypes.LCURLY
385 || prevStmt.getType() == TokenTypes.ARRAY_INIT
386 || prevStmt.getType() == TokenTypes.OBJBLOCK)
387 && nextStmt.getType() == TokenTypes.RCURLY;
388 }
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412 private void handleCommentInEmptyCaseBlock(DetailAST prevStmt, DetailAST comment,
413 DetailAST nextStmt) {
414 if (comment.getColumnNo() < prevStmt.getColumnNo()
415 || comment.getColumnNo() < nextStmt.getColumnNo()) {
416 logMultilineIndentation(prevStmt, comment, nextStmt);
417 }
418 }
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456 private void handleFallThroughComment(DetailAST prevStmt, DetailAST comment,
457 DetailAST nextStmt) {
458 if (!areSameLevelIndented(comment, prevStmt, nextStmt)) {
459 logMultilineIndentation(prevStmt, comment, nextStmt);
460 }
461 }
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481 private void handleCommentAtTheEndOfTheCodeBlock(DetailAST prevStmt, DetailAST comment,
482 DetailAST nextStmt) {
483 if (prevStmt != null) {
484 if (prevStmt.getType() == TokenTypes.LITERAL_CASE
485 || prevStmt.getType() == TokenTypes.CASE_GROUP
486 || prevStmt.getType() == TokenTypes.LITERAL_DEFAULT) {
487 if (comment.getColumnNo() < nextStmt.getColumnNo()) {
488 log(comment, getMessageKey(comment), nextStmt.getLineNo(),
489 comment.getColumnNo(), nextStmt.getColumnNo());
490 }
491 }
492 else if (isCommentForMultiblock(nextStmt)) {
493 if (!areSameLevelIndented(comment, prevStmt, nextStmt)) {
494 logMultilineIndentation(prevStmt, comment, nextStmt);
495 }
496 }
497 else if (!areSameLevelIndented(comment, prevStmt, prevStmt)) {
498 final int prevStmtLineNo = prevStmt.getLineNo();
499 log(comment, getMessageKey(comment), prevStmtLineNo,
500 comment.getColumnNo(), getLineStart(prevStmtLineNo));
501 }
502 }
503 }
504
505
506
507
508
509
510
511
512 private static boolean isCommentForMultiblock(DetailAST endBlockStmt) {
513 final DetailAST nextBlock = endBlockStmt.getParent().getNextSibling();
514 final int endBlockLineNo = endBlockStmt.getLineNo();
515 final DetailAST catchAst = endBlockStmt.getParent().getParent();
516 final DetailAST finallyAst = catchAst.getNextSibling();
517 return nextBlock != null && nextBlock.getLineNo() == endBlockLineNo
518 || finallyAst != null
519 && catchAst.getType() == TokenTypes.LITERAL_CATCH
520 && finallyAst.getLineNo() == endBlockLineNo;
521 }
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542 private void handleCommentInEmptyCodeBlock(DetailAST comment, DetailAST nextStmt) {
543 if (comment.getColumnNo() < nextStmt.getColumnNo()) {
544 log(comment, getMessageKey(comment), nextStmt.getLineNo(),
545 comment.getColumnNo(), nextStmt.getColumnNo());
546 }
547 }
548
549
550
551
552
553
554
555
556
557
558 private DetailAST getOneLinePreviousStatement(DetailAST comment) {
559 DetailAST root = comment.getParent();
560 while (root != null && !isBlockStart(root)) {
561 root = root.getParent();
562 }
563
564 final Deque<DetailAST> stack = new ArrayDeque<>();
565 DetailAST previousStatement = null;
566 while (root != null || !stack.isEmpty()) {
567 if (!stack.isEmpty()) {
568 root = stack.pop();
569 }
570 while (root != null) {
571 previousStatement = findPreviousStatement(comment, root);
572 if (previousStatement != null) {
573 root = null;
574 stack.clear();
575 break;
576 }
577 if (root.getNextSibling() != null) {
578 stack.push(root.getNextSibling());
579 }
580 root = root.getFirstChild();
581 }
582 }
583 return previousStatement;
584 }
585
586
587
588
589
590
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
602
603
604
605
606 private static boolean isBlockStart(DetailAST root) {
607 return root.getType() == TokenTypes.SLIST
608 || root.getType() == TokenTypes.OBJBLOCK
609 || root.getType() == TokenTypes.ARRAY_INIT
610 || root.getType() == TokenTypes.CASE_GROUP;
611 }
612
613
614
615
616
617
618
619
620
621 private DetailAST findPreviousStatement(DetailAST comment, DetailAST root) {
622 DetailAST previousStatement = null;
623 if (Math.max(root.getLineNo(), comment.getLineNo()) == root.getLineNo()) {
624
625
626 previousStatement = getPrevStatementFromSwitchBlock(comment);
627 }
628 final DetailAST tokenWhichBeginsTheLine;
629 if (root.getType() == TokenTypes.EXPR) {
630 tokenWhichBeginsTheLine = findStartTokenOfMethodCallChain(root);
631 }
632 else if (root.getType() == TokenTypes.PLUS) {
633 tokenWhichBeginsTheLine = root.getFirstChild();
634 }
635 else {
636 tokenWhichBeginsTheLine = root;
637 }
638 if (tokenWhichBeginsTheLine != null
639 && !isComment(tokenWhichBeginsTheLine)
640 && isOnPreviousLineIgnoringComments(comment, tokenWhichBeginsTheLine)) {
641 previousStatement = tokenWhichBeginsTheLine;
642 }
643 return previousStatement;
644 }
645
646
647
648
649
650
651
652 private static DetailAST findStartTokenOfMethodCallChain(DetailAST root) {
653 DetailAST startOfMethodCallChain = root;
654 while (startOfMethodCallChain.getFirstChild() != null
655 && TokenUtil.areOnSameLine(startOfMethodCallChain.getFirstChild(), root)) {
656 startOfMethodCallChain = startOfMethodCallChain.getFirstChild();
657 }
658 if (startOfMethodCallChain.getFirstChild() != null) {
659 startOfMethodCallChain = startOfMethodCallChain.getFirstChild().getNextSibling();
660 }
661 return startOfMethodCallChain;
662 }
663
664
665
666
667
668
669
670
671
672
673 private boolean isOnPreviousLineIgnoringComments(DetailAST currentStatement,
674 DetailAST checkedStatement) {
675 DetailAST nextToken = getNextToken(checkedStatement);
676 int distanceAim = 1;
677 if (nextToken != null && isComment(nextToken)) {
678 distanceAim += countEmptyLines(checkedStatement, currentStatement);
679 }
680
681 while (nextToken != null && nextToken != currentStatement && isComment(nextToken)) {
682 if (nextToken.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
683 distanceAim += nextToken.getLastChild().getLineNo() - nextToken.getLineNo();
684 }
685 distanceAim++;
686 nextToken = nextToken.getNextSibling();
687 }
688 return currentStatement.getLineNo() - checkedStatement.getLineNo() == distanceAim;
689 }
690
691
692
693
694
695
696
697 private DetailAST getNextToken(DetailAST checkedStatement) {
698 DetailAST nextToken;
699 if (checkedStatement.getType() == TokenTypes.SLIST
700 || checkedStatement.getType() == TokenTypes.ARRAY_INIT
701 || checkedStatement.getType() == TokenTypes.CASE_GROUP) {
702 nextToken = checkedStatement.getFirstChild();
703 }
704 else {
705 nextToken = checkedStatement.getNextSibling();
706 }
707 if (nextToken != null && isComment(nextToken) && isTrailingComment(nextToken)) {
708 nextToken = nextToken.getNextSibling();
709 }
710 return nextToken;
711 }
712
713
714
715
716
717
718
719
720 private int countEmptyLines(DetailAST startStatement, DetailAST endStatement) {
721 int emptyLinesNumber = 0;
722 final String[] lines = getLines();
723 final int endLineNo = endStatement.getLineNo();
724 for (int lineNo = startStatement.getLineNo(); lineNo < endLineNo; lineNo++) {
725 if (CommonUtil.isBlank(lines[lineNo])) {
726 emptyLinesNumber++;
727 }
728 }
729 return emptyLinesNumber;
730 }
731
732
733
734
735
736
737
738
739 private void logMultilineIndentation(DetailAST prevStmt, DetailAST comment,
740 DetailAST nextStmt) {
741 final String multilineNoTemplate = "%d, %d";
742 log(comment, getMessageKey(comment),
743 String.format(Locale.getDefault(), multilineNoTemplate, prevStmt.getLineNo(),
744 nextStmt.getLineNo()), comment.getColumnNo(),
745 String.format(Locale.getDefault(), multilineNoTemplate,
746 getLineStart(prevStmt.getLineNo()), getLineStart(nextStmt.getLineNo())));
747 }
748
749
750
751
752
753
754
755 private static String getMessageKey(DetailAST comment) {
756 final String msgKey;
757 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
758 msgKey = MSG_KEY_SINGLE;
759 }
760 else {
761 msgKey = MSG_KEY_BLOCK;
762 }
763 return msgKey;
764 }
765
766
767
768
769
770
771
772 private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) {
773 final DetailAST prevStmt;
774 final DetailAST parentStatement = comment.getParent();
775 if (parentStatement.getType() == TokenTypes.CASE_GROUP) {
776 prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement);
777 }
778 else {
779 prevStmt = getPrevCaseToken(parentStatement);
780 }
781 return prevStmt;
782 }
783
784
785
786
787
788
789
790 private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) {
791 DetailAST prevStmt = null;
792 final DetailAST prevBlock = parentStatement.getPreviousSibling();
793 if (prevBlock.getLastChild() != null) {
794 DetailAST blockBody = prevBlock.getLastChild().getLastChild();
795 if (blockBody.getType() == TokenTypes.SEMI) {
796 blockBody = blockBody.getPreviousSibling();
797 }
798 if (blockBody.getType() == TokenTypes.EXPR) {
799 prevStmt = findStartTokenOfMethodCallChain(blockBody);
800 }
801 else {
802 if (blockBody.getType() == TokenTypes.SLIST) {
803 prevStmt = blockBody.getParent().getParent();
804 }
805 else {
806 prevStmt = blockBody;
807 }
808 }
809 if (isComment(prevStmt)) {
810 prevStmt = prevStmt.getNextSibling();
811 }
812 }
813 return prevStmt;
814 }
815
816
817
818
819
820
821
822 private static DetailAST getPrevCaseToken(DetailAST parentStatement) {
823 final DetailAST prevCaseToken;
824 final DetailAST parentBlock = parentStatement.getParent();
825 if (parentBlock.getParent().getPreviousSibling() != null
826 && parentBlock.getParent().getPreviousSibling().getType()
827 == TokenTypes.LITERAL_CASE) {
828 prevCaseToken = parentBlock.getParent().getPreviousSibling();
829 }
830 else {
831 prevCaseToken = null;
832 }
833 return prevCaseToken;
834 }
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858 private boolean areSameLevelIndented(DetailAST comment, DetailAST prevStmt,
859 DetailAST nextStmt) {
860 return comment.getColumnNo() == getLineStart(nextStmt.getLineNo())
861 || comment.getColumnNo() == getLineStart(prevStmt.getLineNo());
862 }
863
864
865
866
867
868
869
870 private int getLineStart(int lineNo) {
871 final char[] line = getLines()[lineNo - 1].toCharArray();
872 int lineStart = 0;
873 while (Character.isWhitespace(line[lineStart])) {
874 lineStart++;
875 }
876 return lineStart;
877 }
878
879
880
881
882
883
884
885 private boolean isTrailingComment(DetailAST comment) {
886 final boolean isTrailingComment;
887 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
888 isTrailingComment = isTrailingSingleLineComment(comment);
889 }
890 else {
891 isTrailingComment = isTrailingBlockComment(comment);
892 }
893 return isTrailingComment;
894 }
895
896
897
898
899
900
901
902
903
904
905
906
907
908 private boolean isTrailingSingleLineComment(DetailAST singleLineComment) {
909 final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1);
910 final int commentColumnNo = singleLineComment.getColumnNo();
911 return !CommonUtil.hasWhitespaceBefore(commentColumnNo, targetSourceLine);
912 }
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927 private boolean isTrailingBlockComment(DetailAST blockComment) {
928 final String commentLine = getLine(blockComment.getLineNo() - 1);
929 final int commentColumnNo = blockComment.getColumnNo();
930 final DetailAST nextSibling = blockComment.getNextSibling();
931 return !CommonUtil.hasWhitespaceBefore(commentColumnNo, commentLine)
932 || nextSibling != null && TokenUtil.areOnSameLine(nextSibling, blockComment);
933 }
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954 private static boolean areInSameMethodCallWithSameIndent(DetailAST comment) {
955 return comment.getParent().getType() == TokenTypes.METHOD_CALL
956 && comment.getColumnNo()
957 == getFirstExpressionNodeFromMethodCall(comment.getParent()).getColumnNo();
958 }
959
960
961
962
963
964
965
966 private static DetailAST getFirstExpressionNodeFromMethodCall(DetailAST methodCall) {
967
968 return methodCall.findFirstToken(TokenTypes.ELIST);
969 }
970
971 }