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