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