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