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