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.whitespace;
21
22 import java.util.ArrayList;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Optional;
26
27 import com.puppycrawl.tools.checkstyle.StatelessCheck;
28 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
29 import com.puppycrawl.tools.checkstyle.api.DetailAST;
30 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
32 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
33 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
34 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130 @StatelessCheck
131 public class EmptyLineSeparatorCheck extends AbstractCheck {
132
133
134
135
136
137 public static final String MSG_SHOULD_BE_SEPARATED = "empty.line.separator";
138
139
140
141
142
143
144 public static final String MSG_MULTIPLE_LINES = "empty.line.separator.multiple.lines";
145
146
147
148
149
150 public static final String MSG_MULTIPLE_LINES_AFTER =
151 "empty.line.separator.multiple.lines.after";
152
153
154
155
156
157 public static final String MSG_MULTIPLE_LINES_INSIDE =
158 "empty.line.separator.multiple.lines.inside";
159
160
161 private boolean allowNoEmptyLineBetweenFields;
162
163
164 private boolean allowMultipleEmptyLines = true;
165
166
167 private boolean allowMultipleEmptyLinesInsideClassMembers = true;
168
169
170
171
172
173
174
175
176 public final void setAllowNoEmptyLineBetweenFields(boolean allow) {
177 allowNoEmptyLineBetweenFields = allow;
178 }
179
180
181
182
183
184
185
186 public void setAllowMultipleEmptyLines(boolean allow) {
187 allowMultipleEmptyLines = allow;
188 }
189
190
191
192
193
194
195
196 public void setAllowMultipleEmptyLinesInsideClassMembers(boolean allow) {
197 allowMultipleEmptyLinesInsideClassMembers = allow;
198 }
199
200 @Override
201 public boolean isCommentNodesRequired() {
202 return true;
203 }
204
205 @Override
206 public int[] getDefaultTokens() {
207 return getAcceptableTokens();
208 }
209
210 @Override
211 public int[] getAcceptableTokens() {
212 return new int[] {
213 TokenTypes.PACKAGE_DEF,
214 TokenTypes.IMPORT,
215 TokenTypes.STATIC_IMPORT,
216 TokenTypes.CLASS_DEF,
217 TokenTypes.INTERFACE_DEF,
218 TokenTypes.ENUM_DEF,
219 TokenTypes.STATIC_INIT,
220 TokenTypes.INSTANCE_INIT,
221 TokenTypes.METHOD_DEF,
222 TokenTypes.CTOR_DEF,
223 TokenTypes.VARIABLE_DEF,
224 TokenTypes.RECORD_DEF,
225 TokenTypes.COMPACT_CTOR_DEF,
226 };
227 }
228
229 @Override
230 public int[] getRequiredTokens() {
231 return CommonUtil.EMPTY_INT_ARRAY;
232 }
233
234 @Override
235 public void visitToken(DetailAST ast) {
236 checkComments(ast);
237 if (hasMultipleLinesBefore(ast)) {
238 log(ast, MSG_MULTIPLE_LINES, ast.getText());
239 }
240 if (!allowMultipleEmptyLinesInsideClassMembers) {
241 processMultipleLinesInside(ast);
242 }
243 if (ast.getType() == TokenTypes.PACKAGE_DEF) {
244 checkCommentInModifiers(ast);
245 }
246 DetailAST nextToken = ast.getNextSibling();
247 while (nextToken != null && TokenUtil.isCommentType(nextToken.getType())) {
248 nextToken = nextToken.getNextSibling();
249 }
250 if (nextToken != null) {
251 checkToken(ast, nextToken);
252 }
253 }
254
255
256
257
258
259
260
261 private void checkToken(DetailAST ast, DetailAST nextToken) {
262 final int astType = ast.getType();
263
264 switch (astType) {
265 case TokenTypes.VARIABLE_DEF -> processVariableDef(ast, nextToken);
266
267 case TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT -> processImport(ast, nextToken);
268
269 case TokenTypes.PACKAGE_DEF -> processPackage(ast, nextToken);
270
271 default -> {
272 if (nextToken.getType() == TokenTypes.RCURLY) {
273 if (hasNotAllowedTwoEmptyLinesBefore(nextToken)) {
274 final DetailAST result = getLastElementBeforeEmptyLines(
275 ast, nextToken.getLineNo()
276 );
277 log(result, MSG_MULTIPLE_LINES_AFTER, result.getText());
278 }
279 }
280 else if (!hasEmptyLineAfter(ast)) {
281 log(nextToken, MSG_SHOULD_BE_SEPARATED, nextToken.getText());
282 }
283 }
284 }
285 }
286
287
288
289
290
291
292 private void checkCommentInModifiers(DetailAST packageDef) {
293 final Optional<DetailAST> comment = findCommentUnder(packageDef);
294 comment.ifPresent(commentValue -> {
295 log(commentValue, MSG_SHOULD_BE_SEPARATED, commentValue.getText());
296 });
297 }
298
299
300
301
302
303
304
305 private void processMultipleLinesInside(DetailAST ast) {
306 final int astType = ast.getType();
307 if (isClassMemberBlock(astType)) {
308 final List<Integer> emptyLines = getEmptyLines(ast);
309 final List<Integer> emptyLinesToLog = getEmptyLinesToLog(emptyLines);
310 for (Integer lineNo : emptyLinesToLog) {
311 log(getLastElementBeforeEmptyLines(ast, lineNo), MSG_MULTIPLE_LINES_INSIDE);
312 }
313 }
314 }
315
316
317
318
319
320
321
322
323 private static DetailAST getLastElementBeforeEmptyLines(DetailAST ast, int line) {
324 DetailAST result = ast;
325 if (ast.getFirstChild().getLineNo() <= line) {
326 result = ast.getFirstChild();
327 while (result.getNextSibling() != null
328 && result.getNextSibling().getLineNo() <= line) {
329 result = result.getNextSibling();
330 }
331 if (result.hasChildren()) {
332 result = getLastElementBeforeEmptyLines(result, line);
333 }
334 }
335
336 if (result.getNextSibling() != null) {
337 final Optional<DetailAST> postFixNode = getPostFixNode(result.getNextSibling());
338 if (postFixNode.isPresent()) {
339
340
341
342
343
344 final DetailAST firstChildAfterPostFix = postFixNode.orElseThrow();
345 result = getLastElementBeforeEmptyLines(firstChildAfterPostFix, line);
346 }
347 }
348 return result;
349 }
350
351
352
353
354
355
356
357 private static Optional<DetailAST> getPostFixNode(DetailAST ast) {
358 Optional<DetailAST> result = Optional.empty();
359 if (ast.getType() == TokenTypes.EXPR
360
361 && ast.getFirstChild().getType() == TokenTypes.METHOD_CALL) {
362
363 final DetailAST node = ast.getFirstChild().getFirstChild();
364 if (node.getType() == TokenTypes.DOT) {
365 result = Optional.of(node);
366 }
367 }
368 return result;
369 }
370
371
372
373
374
375
376
377 private static boolean isClassMemberBlock(int astType) {
378 return TokenUtil.isOfType(astType,
379 TokenTypes.STATIC_INIT, TokenTypes.INSTANCE_INIT, TokenTypes.METHOD_DEF,
380 TokenTypes.CTOR_DEF, TokenTypes.COMPACT_CTOR_DEF);
381 }
382
383
384
385
386
387
388
389 private List<Integer> getEmptyLines(DetailAST ast) {
390 final DetailAST lastToken = ast.getLastChild().getLastChild();
391 int lastTokenLineNo = 0;
392 if (lastToken != null) {
393
394
395 lastTokenLineNo = lastToken.getLineNo() - 2;
396 }
397 final List<Integer> emptyLines = new ArrayList<>();
398
399 for (int lineNo = ast.getLineNo(); lineNo <= lastTokenLineNo; lineNo++) {
400 if (CommonUtil.isBlank(getLine(lineNo))) {
401 emptyLines.add(lineNo);
402 }
403 }
404 return emptyLines;
405 }
406
407
408
409
410
411
412
413 private static List<Integer> getEmptyLinesToLog(Iterable<Integer> emptyLines) {
414 final List<Integer> emptyLinesToLog = new ArrayList<>();
415 int previousEmptyLineNo = -1;
416 for (int emptyLineNo : emptyLines) {
417 if (previousEmptyLineNo + 1 == emptyLineNo) {
418 emptyLinesToLog.add(previousEmptyLineNo);
419 }
420 previousEmptyLineNo = emptyLineNo;
421 }
422 return emptyLinesToLog;
423 }
424
425
426
427
428
429
430
431 private boolean hasMultipleLinesBefore(DetailAST ast) {
432 return (ast.getType() != TokenTypes.VARIABLE_DEF || isTypeField(ast))
433 && hasNotAllowedTwoEmptyLinesBefore(ast);
434 }
435
436
437
438
439
440
441
442 private void processPackage(DetailAST ast, DetailAST nextToken) {
443 if (ast.getLineNo() > 1 && !hasEmptyLineBefore(ast)) {
444 if (CheckUtil.isPackageInfo(getFilePath())) {
445 if (!ast.getFirstChild().hasChildren() && !isPrecededByJavadoc(ast)) {
446 log(ast, MSG_SHOULD_BE_SEPARATED, ast.getText());
447 }
448 }
449 else {
450 log(ast, MSG_SHOULD_BE_SEPARATED, ast.getText());
451 }
452 }
453 if (isLineEmptyAfterPackage(ast)) {
454 final DetailAST elementAst = getViolationAstForPackage(ast);
455 log(elementAst, MSG_SHOULD_BE_SEPARATED, elementAst.getText());
456 }
457 else if (!hasEmptyLineAfter(ast)) {
458 log(nextToken, MSG_SHOULD_BE_SEPARATED, nextToken.getText());
459 }
460 }
461
462
463
464
465
466
467
468 private static boolean isLineEmptyAfterPackage(DetailAST ast) {
469 DetailAST nextElement = ast;
470 final int lastChildLineNo = ast.getLastChild().getLineNo();
471 while (nextElement.getLineNo() < lastChildLineNo + 1
472 && nextElement.getNextSibling() != null) {
473 nextElement = nextElement.getNextSibling();
474 }
475 return nextElement.getLineNo() == lastChildLineNo + 1;
476 }
477
478
479
480
481
482
483
484 private static DetailAST getViolationAstForPackage(DetailAST ast) {
485 DetailAST nextElement = ast;
486 final int lastChildLineNo = ast.getLastChild().getLineNo();
487 while (nextElement.getLineNo() < lastChildLineNo + 1) {
488 nextElement = nextElement.getNextSibling();
489 }
490 return nextElement;
491 }
492
493
494
495
496
497
498
499 private void processImport(DetailAST ast, DetailAST nextToken) {
500 if (!TokenUtil.isOfType(nextToken, TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT)
501 && !hasEmptyLineAfter(ast)) {
502 log(nextToken, MSG_SHOULD_BE_SEPARATED, nextToken.getText());
503 }
504 }
505
506
507
508
509
510
511
512 private void processVariableDef(DetailAST ast, DetailAST nextToken) {
513 if (isTypeField(ast) && !hasEmptyLineAfter(ast)
514 && isViolatingEmptyLineBetweenFieldsPolicy(nextToken)) {
515 log(nextToken, MSG_SHOULD_BE_SEPARATED,
516 nextToken.getText());
517 }
518 }
519
520
521
522
523
524
525
526 private boolean isViolatingEmptyLineBetweenFieldsPolicy(DetailAST detailAST) {
527 return detailAST.getType() != TokenTypes.RCURLY
528 && (!allowNoEmptyLineBetweenFields
529 || !TokenUtil.isOfType(detailAST, TokenTypes.COMMA, TokenTypes.VARIABLE_DEF));
530 }
531
532
533
534
535
536
537
538 private boolean hasNotAllowedTwoEmptyLinesBefore(DetailAST token) {
539 return !allowMultipleEmptyLines && hasEmptyLineBefore(token)
540 && isPrePreviousLineEmpty(token);
541 }
542
543
544
545
546
547
548 private void checkComments(DetailAST token) {
549 if (!allowMultipleEmptyLines) {
550 if (TokenUtil.isOfType(token,
551 TokenTypes.PACKAGE_DEF, TokenTypes.IMPORT,
552 TokenTypes.STATIC_IMPORT, TokenTypes.STATIC_INIT)) {
553 DetailAST previousNode = token.getPreviousSibling();
554 while (isCommentInBeginningOfLine(previousNode)) {
555 if (hasEmptyLineBefore(previousNode) && isPrePreviousLineEmpty(previousNode)) {
556 log(previousNode, MSG_MULTIPLE_LINES, previousNode.getText());
557 }
558 previousNode = previousNode.getPreviousSibling();
559 }
560 }
561 else {
562 checkCommentsInsideToken(token);
563 }
564 }
565 }
566
567
568
569
570
571
572
573 private void checkCommentsInsideToken(DetailAST token) {
574 final List<DetailAST> childNodes = new LinkedList<>();
575 DetailAST childNode = token.getLastChild();
576 while (childNode != null) {
577 if (childNode.getType() == TokenTypes.MODIFIERS) {
578 for (DetailAST node = token.getFirstChild().getLastChild();
579 node != null;
580 node = node.getPreviousSibling()) {
581 if (isCommentInBeginningOfLine(node)) {
582 childNodes.add(node);
583 }
584 }
585 }
586 else if (isCommentInBeginningOfLine(childNode)) {
587 childNodes.add(childNode);
588 }
589 childNode = childNode.getPreviousSibling();
590 }
591 for (DetailAST node : childNodes) {
592 if (hasEmptyLineBefore(node) && isPrePreviousLineEmpty(node)) {
593 log(node, MSG_MULTIPLE_LINES, node.getText());
594 }
595 }
596 }
597
598
599
600
601
602
603
604 private boolean isPrePreviousLineEmpty(DetailAST token) {
605 boolean result = false;
606 final int lineNo = token.getLineNo();
607
608 final int number = 3;
609 if (lineNo >= number) {
610 final String prePreviousLine = getLine(lineNo - number);
611
612 result = CommonUtil.isBlank(prePreviousLine);
613 final boolean previousLineIsEmpty = CommonUtil.isBlank(getLine(lineNo - 2));
614
615 if (previousLineIsEmpty && result) {
616 result = true;
617 }
618 else if (token.findFirstToken(TokenTypes.TYPE) != null) {
619 result = isTwoPrecedingPreviousLinesFromCommentEmpty(token);
620 }
621 }
622 return result;
623
624 }
625
626
627
628
629
630
631
632 private boolean isTwoPrecedingPreviousLinesFromCommentEmpty(DetailAST token) {
633 boolean upToPrePreviousLinesEmpty = false;
634
635 for (DetailAST typeChild = token.findFirstToken(TokenTypes.TYPE).getLastChild();
636 typeChild != null; typeChild = typeChild.getPreviousSibling()) {
637
638 if (isTokenNotOnPreviousSiblingLines(typeChild, token)) {
639
640 final String commentBeginningPreviousLine =
641 getLine(typeChild.getLineNo() - 2);
642 final String commentBeginningPrePreviousLine =
643 getLine(typeChild.getLineNo() - 3);
644
645 if (CommonUtil.isBlank(commentBeginningPreviousLine)
646 && CommonUtil.isBlank(commentBeginningPrePreviousLine)) {
647 upToPrePreviousLinesEmpty = true;
648 break;
649 }
650
651 }
652
653 }
654
655 return upToPrePreviousLinesEmpty;
656 }
657
658
659
660
661
662
663
664
665 private static boolean isTokenNotOnPreviousSiblingLines(DetailAST token,
666 DetailAST parentToken) {
667 DetailAST previousSibling = parentToken.getPreviousSibling();
668 for (DetailAST astNode = previousSibling; astNode != null;
669 astNode = astNode.getLastChild()) {
670 previousSibling = astNode;
671 }
672
673 return token.getLineNo() != previousSibling.getLineNo();
674 }
675
676
677
678
679
680
681
682 private boolean hasEmptyLineAfter(DetailAST token) {
683 DetailAST lastToken = token.getLastChild().getLastChild();
684 if (lastToken == null) {
685 lastToken = token.getLastChild();
686 }
687 DetailAST nextToken = token.getNextSibling();
688 if (TokenUtil.isCommentType(nextToken.getType())) {
689 nextToken = nextToken.getNextSibling();
690 }
691
692 final int nextBegin = nextToken.getLineNo();
693
694 final int currentEnd = lastToken.getLineNo();
695 return hasEmptyLine(currentEnd + 1, nextBegin - 1);
696 }
697
698
699
700
701
702
703
704 private static Optional<DetailAST> findCommentUnder(DetailAST packageDef) {
705 return Optional.ofNullable(packageDef.getNextSibling())
706 .map(sibling -> sibling.findFirstToken(TokenTypes.MODIFIERS))
707 .map(DetailAST::getFirstChild)
708 .filter(token -> TokenUtil.isCommentType(token.getType()))
709 .filter(comment -> comment.getLineNo() == packageDef.getLineNo() + 1);
710 }
711
712
713
714
715
716
717
718
719
720
721 private boolean hasEmptyLine(int startLine, int endLine) {
722
723 boolean result = false;
724 for (int line = startLine; line <= endLine; line++) {
725
726 if (CommonUtil.isBlank(getLine(line - 1))) {
727 result = true;
728 break;
729 }
730 }
731 return result;
732 }
733
734
735
736
737
738
739
740 private boolean hasEmptyLineBefore(DetailAST token) {
741 boolean result = false;
742 final int lineNo = token.getLineNo();
743 if (lineNo != 1) {
744
745 final String lineBefore = getLine(lineNo - 2);
746
747 if (CommonUtil.isBlank(lineBefore)) {
748 result = true;
749 }
750 else if (token.findFirstToken(TokenTypes.TYPE) != null) {
751 for (DetailAST typeChild = token.findFirstToken(TokenTypes.TYPE).getLastChild();
752 typeChild != null && !result && typeChild.getLineNo() > 1;
753 typeChild = typeChild.getPreviousSibling()) {
754
755 final String commentBeginningPreviousLine =
756 getLine(typeChild.getLineNo() - 2);
757 result = CommonUtil.isBlank(commentBeginningPreviousLine);
758
759 }
760 }
761 }
762 return result;
763 }
764
765
766
767
768
769
770
771 private boolean isCommentInBeginningOfLine(DetailAST comment) {
772
773
774 boolean result = false;
775 if (comment != null) {
776 final String lineWithComment = getLine(comment.getLineNo() - 1).trim();
777 result = lineWithComment.startsWith("//") || lineWithComment.startsWith("/*");
778 }
779 return result;
780 }
781
782
783
784
785
786
787
788 private static boolean isPrecededByJavadoc(DetailAST token) {
789 boolean result = false;
790 final DetailAST previous = token.getPreviousSibling();
791 if (previous.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
792 && JavadocUtil.isJavadocComment(previous.getFirstChild().getText())) {
793 result = true;
794 }
795 return result;
796 }
797
798
799
800
801
802
803
804 private static boolean isTypeField(DetailAST variableDef) {
805 return TokenUtil.isTypeDeclaration(variableDef.getParent().getParent().getType());
806 }
807
808 }