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