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