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