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