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