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 }