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