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