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.imports;
21
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.List;
25 import java.util.StringTokenizer;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31 import com.puppycrawl.tools.checkstyle.api.DetailAST;
32 import com.puppycrawl.tools.checkstyle.api.FullIdent;
33 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
34 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217 @FileStatefulCheck
218 public class CustomImportOrderCheck extends AbstractCheck {
219
220
221
222
223
224 public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator";
225
226
227
228
229
230 public static final String MSG_SEPARATED_IN_GROUP = "custom.import.order.separated.internally";
231
232
233
234
235
236 public static final String MSG_LEX = "custom.import.order.lex";
237
238
239
240
241
242 public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import";
243
244
245
246
247
248 public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected";
249
250
251
252
253
254 public static final String MSG_ORDER = "custom.import.order";
255
256
257 public static final String STATIC_RULE_GROUP = "STATIC";
258
259
260 public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE";
261
262
263 public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE";
264
265
266 public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE";
267
268
269 public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS";
270
271
272 private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP";
273
274
275 private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
276
277
278 private final List<String> customImportOrderRules = new ArrayList<>();
279
280
281 private final List<ImportDetails> importToGroupList = new ArrayList<>();
282
283
284 private String samePackageDomainsRegExp = "";
285
286
287 private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
288
289
290 private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
291
292
293 private Pattern specialImportsRegExp = Pattern.compile("^$");
294
295
296 private boolean separateLineBetweenGroups = true;
297
298
299
300
301
302 private boolean sortImportsInGroupAlphabetically;
303
304
305 private int samePackageMatchingDepth;
306
307
308
309
310
311
312
313
314 public final void setStandardPackageRegExp(Pattern regexp) {
315 standardPackageRegExp = regexp;
316 }
317
318
319
320
321
322
323
324
325 public final void setThirdPartyPackageRegExp(Pattern regexp) {
326 thirdPartyPackageRegExp = regexp;
327 }
328
329
330
331
332
333
334
335
336 public final void setSpecialImportsRegExp(Pattern regexp) {
337 specialImportsRegExp = regexp;
338 }
339
340
341
342
343
344
345
346
347 public final void setSeparateLineBetweenGroups(boolean value) {
348 separateLineBetweenGroups = value;
349 }
350
351
352
353
354
355
356
357
358
359 public final void setSortImportsInGroupAlphabetically(boolean value) {
360 sortImportsInGroupAlphabetically = value;
361 }
362
363
364
365
366
367
368
369
370 public final void setCustomImportOrderRules(String... rules) {
371 Arrays.stream(rules)
372 .map(GROUP_SEPARATOR_PATTERN::split)
373 .flatMap(Arrays::stream)
374 .forEach(this::addRulesToList);
375
376 customImportOrderRules.add(NON_GROUP_RULE_GROUP);
377 }
378
379 @Override
380 public int[] getDefaultTokens() {
381 return getRequiredTokens();
382 }
383
384 @Override
385 public int[] getAcceptableTokens() {
386 return getRequiredTokens();
387 }
388
389 @Override
390 public int[] getRequiredTokens() {
391 return new int[] {
392 TokenTypes.IMPORT,
393 TokenTypes.STATIC_IMPORT,
394 TokenTypes.PACKAGE_DEF,
395 };
396 }
397
398 @Override
399 public void beginTree(DetailAST rootAST) {
400 importToGroupList.clear();
401 }
402
403 @Override
404 public void visitToken(DetailAST ast) {
405 if (ast.getType() == TokenTypes.PACKAGE_DEF) {
406 samePackageDomainsRegExp = createSamePackageRegexp(
407 samePackageMatchingDepth, ast);
408 }
409 else {
410 final String importFullPath = getFullImportIdent(ast);
411 final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
412 importToGroupList.add(new ImportDetails(importFullPath,
413 getImportGroup(isStatic, importFullPath), isStatic, ast));
414 }
415 }
416
417 @Override
418 public void finishTree(DetailAST rootAST) {
419 if (!importToGroupList.isEmpty()) {
420 finishImportList();
421 }
422 }
423
424
425 private void finishImportList() {
426 String currentGroup = getFirstGroup();
427 int currentGroupNumber = customImportOrderRules.lastIndexOf(currentGroup);
428 ImportDetails previousImportObjectFromCurrentGroup = null;
429 String previousImportFromCurrentGroup = null;
430
431 for (ImportDetails importObject : importToGroupList) {
432 final String importGroup = importObject.getImportGroup();
433 final String fullImportIdent = importObject.getImportFullPath();
434
435 if (importGroup.equals(currentGroup)) {
436 validateExtraEmptyLine(previousImportObjectFromCurrentGroup,
437 importObject, fullImportIdent);
438 if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) {
439 log(importObject.getImportAST(), MSG_LEX,
440 fullImportIdent, previousImportFromCurrentGroup);
441 }
442 else {
443 previousImportFromCurrentGroup = fullImportIdent;
444 }
445 previousImportObjectFromCurrentGroup = importObject;
446 }
447 else {
448
449 if (customImportOrderRules.size() > currentGroupNumber + 1) {
450 final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
451 if (importGroup.equals(nextGroup)) {
452 validateMissedEmptyLine(previousImportObjectFromCurrentGroup,
453 importObject, fullImportIdent);
454 currentGroup = nextGroup;
455 currentGroupNumber = customImportOrderRules.lastIndexOf(nextGroup);
456 previousImportFromCurrentGroup = fullImportIdent;
457 }
458 else {
459 logWrongImportGroupOrder(importObject.getImportAST(),
460 importGroup, nextGroup, fullImportIdent);
461 }
462 previousImportObjectFromCurrentGroup = importObject;
463 }
464 else {
465 logWrongImportGroupOrder(importObject.getImportAST(),
466 importGroup, currentGroup, fullImportIdent);
467 }
468 }
469 }
470 }
471
472
473
474
475
476
477
478
479 private void validateMissedEmptyLine(ImportDetails previousImport,
480 ImportDetails importObject, String fullImportIdent) {
481 if (isEmptyLineMissed(previousImport, importObject)) {
482 log(importObject.getImportAST(), MSG_LINE_SEPARATOR, fullImportIdent);
483 }
484 }
485
486
487
488
489
490
491
492
493 private void validateExtraEmptyLine(ImportDetails previousImport,
494 ImportDetails importObject, String fullImportIdent) {
495 if (isSeparatedByExtraEmptyLine(previousImport, importObject)) {
496 log(importObject.getImportAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent);
497 }
498 }
499
500
501
502
503
504
505
506 private String getFirstGroup() {
507 final ImportDetails firstImport = importToGroupList.get(0);
508 return getImportGroup(firstImport.isStaticImport(),
509 firstImport.getImportFullPath());
510 }
511
512
513
514
515
516
517
518
519
520
521
522 private boolean isAlphabeticalOrderBroken(String previousImport,
523 String currentImport) {
524 return sortImportsInGroupAlphabetically
525 && previousImport != null
526 && compareImports(currentImport, previousImport) < 0;
527 }
528
529
530
531
532
533
534
535
536
537
538
539 private boolean isEmptyLineMissed(ImportDetails previousImportObject,
540 ImportDetails currentImportObject) {
541 return separateLineBetweenGroups
542 && getCountOfEmptyLinesBetween(
543 previousImportObject.getEndLineNumber(),
544 currentImportObject.getStartLineNumber()) != 1;
545 }
546
547
548
549
550
551
552
553
554
555
556
557 private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject,
558 ImportDetails currentImportObject) {
559 return previousImportObject != null
560 && getCountOfEmptyLinesBetween(
561 previousImportObject.getEndLineNumber(),
562 currentImportObject.getStartLineNumber()) > 0;
563 }
564
565
566
567
568
569
570
571
572
573
574
575
576
577 private void logWrongImportGroupOrder(DetailAST importAST, String importGroup,
578 String currentGroupNumber, String fullImportIdent) {
579 if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
580 log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent);
581 }
582 else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
583 log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
584 }
585 else {
586 log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
587 }
588 }
589
590
591
592
593
594
595
596
597
598 private String getNextImportGroup(int currentGroupNumber) {
599 int nextGroupNumber = currentGroupNumber;
600
601 while (customImportOrderRules.size() > nextGroupNumber + 1) {
602 if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
603 break;
604 }
605 nextGroupNumber++;
606 }
607 return customImportOrderRules.get(nextGroupNumber);
608 }
609
610
611
612
613
614
615
616
617
618 private boolean hasAnyImportInCurrentGroup(String currentGroup) {
619 boolean result = false;
620 for (ImportDetails currentImport : importToGroupList) {
621 if (currentGroup.equals(currentImport.getImportGroup())) {
622 result = true;
623 break;
624 }
625 }
626 return result;
627 }
628
629
630
631
632
633
634
635
636
637
638 private String getImportGroup(boolean isStatic, String importPath) {
639 RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
640 if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
641 bestMatch.group = STATIC_RULE_GROUP;
642 bestMatch.matchLength = importPath.length();
643 }
644 else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
645 final String importPathTrimmedToSamePackageDepth =
646 getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
647 if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
648 bestMatch.group = SAME_PACKAGE_RULE_GROUP;
649 bestMatch.matchLength = importPath.length();
650 }
651 }
652 for (String group : customImportOrderRules) {
653 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
654 bestMatch = findBetterPatternMatch(importPath,
655 STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
656 }
657 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
658 bestMatch = findBetterPatternMatch(importPath,
659 group, specialImportsRegExp, bestMatch);
660 }
661 }
662
663 if (NON_GROUP_RULE_GROUP.equals(bestMatch.group)
664 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
665 && thirdPartyPackageRegExp.matcher(importPath).find()) {
666 bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
667 }
668 return bestMatch.group;
669 }
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686 private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
687 Pattern regExp, RuleMatchForImport currentBestMatch) {
688 RuleMatchForImport betterMatchCandidate = currentBestMatch;
689 final Matcher matcher = regExp.matcher(importPath);
690 while (matcher.find()) {
691 final int matchStart = matcher.start();
692 final int length = matcher.end() - matchStart;
693 if (length > betterMatchCandidate.matchLength
694 || length == betterMatchCandidate.matchLength
695 && matchStart < betterMatchCandidate.matchPosition) {
696 betterMatchCandidate = new RuleMatchForImport(group, length, matchStart);
697 }
698 }
699 return betterMatchCandidate;
700 }
701
702
703
704
705
706
707
708
709
710
711
712
713 private static int compareImports(String import1, String import2) {
714 int result = 0;
715 final String separator = "\\.";
716 final String[] import1Tokens = import1.split(separator);
717 final String[] import2Tokens = import2.split(separator);
718 for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
719 final String import1Token = import1Tokens[i];
720 final String import2Token = import2Tokens[i];
721 result = import1Token.compareTo(import2Token);
722 if (result != 0) {
723 break;
724 }
725 }
726 if (result == 0) {
727 result = Integer.compare(import1Tokens.length, import2Tokens.length);
728 }
729 return result;
730 }
731
732
733
734
735
736
737
738
739
740
741
742 private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) {
743 int result = 0;
744 final String[] lines = getLines();
745
746 for (int i = fromLineNo + 1; i <= toLineNo - 1; i++) {
747
748 if (CommonUtil.isBlank(lines[i - 1])) {
749 result++;
750 }
751 }
752 return result;
753 }
754
755
756
757
758
759
760
761
762 private static String getFullImportIdent(DetailAST token) {
763 String ident = "";
764 if (token != null) {
765 ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
766 }
767 return ident;
768 }
769
770
771
772
773
774
775
776
777
778 private void addRulesToList(String ruleStr) {
779 if (STATIC_RULE_GROUP.equals(ruleStr)
780 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
781 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
782 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
783 customImportOrderRules.add(ruleStr);
784 }
785 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
786 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
787 ruleStr.indexOf(')'));
788 samePackageMatchingDepth = Integer.parseInt(rule);
789 if (samePackageMatchingDepth <= 0) {
790 throw new IllegalArgumentException(
791 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
792 }
793 customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
794 }
795 else {
796 throw new IllegalStateException("Unexpected rule: " + ruleStr);
797 }
798 }
799
800
801
802
803
804
805
806
807
808
809 private static String createSamePackageRegexp(int firstPackageDomainsCount,
810 DetailAST packageNode) {
811 final String packageFullPath = getFullImportIdent(packageNode);
812 return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
813 }
814
815
816
817
818
819
820
821
822
823
824
825 private static String getFirstDomainsFromIdent(
826 final int firstPackageDomainsCount, final String packageFullPath) {
827 final StringBuilder builder = new StringBuilder(256);
828 final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
829 int count = firstPackageDomainsCount;
830
831 while (count > 0 && tokens.hasMoreTokens()) {
832 builder.append(tokens.nextToken());
833 count--;
834 }
835 return builder.toString();
836 }
837
838
839
840
841
842 private static final class ImportDetails {
843
844
845 private final String importFullPath;
846
847
848 private final String importGroup;
849
850
851 private final boolean staticImport;
852
853
854 private final DetailAST importAST;
855
856
857
858
859
860
861
862
863
864
865
866
867
868 private ImportDetails(String importFullPath, String importGroup, boolean staticImport,
869 DetailAST importAST) {
870 this.importFullPath = importFullPath;
871 this.importGroup = importGroup;
872 this.staticImport = staticImport;
873 this.importAST = importAST;
874 }
875
876
877
878
879
880
881 public String getImportFullPath() {
882 return importFullPath;
883 }
884
885
886
887
888
889
890 public int getStartLineNumber() {
891 return importAST.getLineNo();
892 }
893
894
895
896
897
898
899
900
901
902
903
904 public int getEndLineNumber() {
905 return importAST.getLastChild().getLineNo();
906 }
907
908
909
910
911
912
913 public String getImportGroup() {
914 return importGroup;
915 }
916
917
918
919
920
921
922 public boolean isStaticImport() {
923 return staticImport;
924 }
925
926
927
928
929
930
931 public DetailAST getImportAST() {
932 return importAST;
933 }
934
935 }
936
937
938
939
940
941 private static final class RuleMatchForImport {
942
943
944 private final int matchPosition;
945
946 private int matchLength;
947
948 private String group;
949
950
951
952
953
954
955
956
957
958
959
960 private RuleMatchForImport(String group, int length, int position) {
961 this.group = group;
962 matchLength = length;
963 matchPosition = position;
964 }
965
966 }
967
968 }