View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.FullIdent;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
34  
35  /**
36   * <div>
37   * Checks that the groups of import declarations appear in the order specified
38   * by the user. If there is an import but its group is not specified in the
39   * configuration such an import should be placed at the end of the import list.
40   * </div>
41   *
42   * <p>
43   * The rule consists of:
44   * </p>
45   * <ol>
46   * <li>
47   * STATIC group. This group sets the ordering of static imports.
48   * </li>
49   * <li>
50   * SAME_PACKAGE(n) group. This group sets the ordering of the same package imports.
51   * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package
52   * name and import name are identical:
53   * {@snippet lang="text" :
54   * package java.util.concurrent.locks;
55   *
56   * import java.io.File;
57   * import java.util.*; //#1
58   * import java.util.List; //#2
59   * import java.util.StringTokenizer; //#3
60   * import java.util.concurrent.*; //#4
61   * import java.util.concurrent.AbstractExecutorService; //#5
62   * import java.util.concurrent.locks.LockSupport; //#6
63   * import java.util.regex.Pattern; //#7
64   * import java.util.regex.Matcher; //#8
65   * }
66   * If we have SAME_PACKAGE(3) on configuration file, imports #4-6 will be considered as
67   * a SAME_PACKAGE group (java.util.concurrent.*, java.util.concurrent.AbstractExecutorService,
68   * java.util.concurrent.locks.LockSupport). SAME_PACKAGE(2) will include #1-8.
69   * SAME_PACKAGE(4) will include only #6. SAME_PACKAGE(5) will result in no imports assigned
70   * to SAME_PACKAGE group because actual package java.util.concurrent.locks has only 4 domains.
71   * </li>
72   * <li>
73   * THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports.
74   * Third party imports are all imports except STATIC, SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and
75   * SPECIAL_IMPORTS.
76   * </li>
77   * <li>
78   * STANDARD_JAVA_PACKAGE group. By default, this group sets ordering of standard java/javax imports.
79   * </li>
80   * <li>
81   * SPECIAL_IMPORTS group. This group may contain some imports that have particular meaning for the
82   * user.
83   * </li>
84   * </ol>
85   *
86   * <p>
87   * Notes:
88   * Rules are configured as a comma-separated ordered list.
89   * </p>
90   *
91   * <p>
92   * Note: '###' group separator is deprecated (in favor of a comma-separated list),
93   * but is currently supported for backward compatibility.
94   * </p>
95   *
96   * <p>
97   * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
98   * thirdPartyPackageRegExp and standardPackageRegExp options.
99   * </p>
100  *
101  * <p>
102  * Pretty often one import can match more than one group. For example, static import from standard
103  * package or regular expressions are configured to allow one import match multiple groups.
104  * In this case, group will be assigned according to priorities:
105  * </p>
106  * <ol>
107  * <li>
108  * STATIC has top priority
109  * </li>
110  * <li>
111  * SAME_PACKAGE has second priority
112  * </li>
113  * <li>
114  * STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS will compete using "best match" rule: longer
115  * matching substring wins; in case of the same length, lower position of matching substring
116  * wins; if position is the same, order of rules in configuration solves the puzzle.
117  * </li>
118  * <li>
119  * THIRD_PARTY has the least priority
120  * </li>
121  * </ol>
122  *
123  * <p>
124  * Few examples to illustrate "best match":
125  * </p>
126  *
127  * <p>
128  * 1. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="ImportOrderCheck" and input file:
129  * </p>
130  * {@snippet lang="text" :
131  * import com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck;
132  * import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck;
133  * }
134  *
135  * <p>
136  * Result: imports will be assigned to SPECIAL_IMPORTS, because matching substring length is 16.
137  * Matching substring for STANDARD_JAVA_PACKAGE is 5.
138  * </p>
139  *
140  * <p>
141  * 2. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="Avoid" and file:
142  * </p>
143  * {@snippet lang="text" :
144  * import com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck;
145  * }
146  *
147  * <p>
148  * Result: import will be assigned to SPECIAL_IMPORTS. Matching substring length is 5 for both
149  * patterns. However, "Avoid" position is lower than "Check" position.
150  * </p>
151  *
152  * @since 5.8
153  */
154 @FileStatefulCheck
155 public class CustomImportOrderCheck extends AbstractCheck {
156 
157     /**
158      * A key is pointing to the warning message text in "messages.properties"
159      * file.
160      */
161     public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator";
162 
163     /**
164      * A key is pointing to the warning message text in "messages.properties"
165      * file.
166      */
167     public static final String MSG_SEPARATED_IN_GROUP = "custom.import.order.separated.internally";
168 
169     /**
170      * A key is pointing to the warning message text in "messages.properties"
171      * file.
172      */
173     public static final String MSG_LEX = "custom.import.order.lex";
174 
175     /**
176      * A key is pointing to the warning message text in "messages.properties"
177      * file.
178      */
179     public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import";
180 
181     /**
182      * A key is pointing to the warning message text in "messages.properties"
183      * file.
184      */
185     public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected";
186 
187     /**
188      * A key is pointing to the warning message text in "messages.properties"
189      * file.
190      */
191     public static final String MSG_ORDER = "custom.import.order";
192 
193     /** STATIC group name. */
194     public static final String STATIC_RULE_GROUP = "STATIC";
195 
196     /** SAME_PACKAGE group name. */
197     public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE";
198 
199     /** THIRD_PARTY_PACKAGE group name. */
200     public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE";
201 
202     /** STANDARD_JAVA_PACKAGE group name. */
203     public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE";
204 
205     /** SPECIAL_IMPORTS group name. */
206     public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS";
207 
208     /** NON_GROUP group name. */
209     private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP";
210 
211     /** Pattern used to separate groups of imports. */
212     private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
213 
214     /** Domain Separator. */
215     private static final String DOMAIN_SEPARATOR = "\\.";
216 
217     /** Specify ordered list of import groups. */
218     private final List<String> customImportOrderRules = new ArrayList<>();
219 
220     /** Contains objects with import attributes. */
221     private final List<ImportDetails> importToGroupList = new ArrayList<>();
222 
223     /** Specify RegExp for SAME_PACKAGE group imports. */
224     private String samePackageDomainsRegExp = "";
225 
226     /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */
227     private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
228 
229     /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */
230     private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
231 
232     /** Specify RegExp for SPECIAL_IMPORTS group imports. */
233     private Pattern specialImportsRegExp = Pattern.compile("^$");
234 
235     /** Force empty line separator between import groups. */
236     private boolean separateLineBetweenGroups = true;
237 
238     /**
239      * Force grouping alphabetically,
240      * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>.
241      */
242     private boolean sortImportsInGroupAlphabetically;
243 
244     /** Number of first domains for SAME_PACKAGE group. */
245     private int samePackageMatchingDepth;
246 
247     /**
248      * Creates a new {@code CustomImportOrderCheck} instance.
249      */
250     public CustomImportOrderCheck() {
251         // no code by default
252     }
253 
254     /**
255      * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports.
256      *
257      * @param regexp
258      *        user value.
259      * @since 5.8
260      */
261     public final void setStandardPackageRegExp(Pattern regexp) {
262         standardPackageRegExp = regexp;
263     }
264 
265     /**
266      * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports.
267      *
268      * @param regexp
269      *        user value.
270      * @since 5.8
271      */
272     public final void setThirdPartyPackageRegExp(Pattern regexp) {
273         thirdPartyPackageRegExp = regexp;
274     }
275 
276     /**
277      * Setter to specify RegExp for SPECIAL_IMPORTS group imports.
278      *
279      * @param regexp
280      *        user value.
281      * @since 5.8
282      */
283     public final void setSpecialImportsRegExp(Pattern regexp) {
284         specialImportsRegExp = regexp;
285     }
286 
287     /**
288      * Setter to force empty line separator between import groups.
289      *
290      * @param value
291      *        user value.
292      * @since 5.8
293      */
294     public final void setSeparateLineBetweenGroups(boolean value) {
295         separateLineBetweenGroups = value;
296     }
297 
298     /**
299      * Setter to force grouping alphabetically, in
300      * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
301      *
302      * @param value
303      *        user value.
304      * @since 5.8
305      */
306     public final void setSortImportsInGroupAlphabetically(boolean value) {
307         sortImportsInGroupAlphabetically = value;
308     }
309 
310     /**
311      * Setter to specify ordered list of import groups.
312      *
313      * @param rules
314      *        user value.
315      * @since 5.8
316      */
317     public final void setCustomImportOrderRules(String... rules) {
318         Arrays.stream(rules)
319                 .map(GROUP_SEPARATOR_PATTERN::split)
320                 .flatMap(Arrays::stream)
321                 .forEach(this::addRulesToList);
322 
323         customImportOrderRules.add(NON_GROUP_RULE_GROUP);
324     }
325 
326     @Override
327     public int[] getDefaultTokens() {
328         return getRequiredTokens();
329     }
330 
331     @Override
332     public int[] getAcceptableTokens() {
333         return getRequiredTokens();
334     }
335 
336     @Override
337     public int[] getRequiredTokens() {
338         return new int[] {
339             TokenTypes.IMPORT,
340             TokenTypes.STATIC_IMPORT,
341             TokenTypes.PACKAGE_DEF,
342         };
343     }
344 
345     @Override
346     public void beginTree(DetailAST rootAST) {
347         importToGroupList.clear();
348     }
349 
350     @Override
351     public void visitToken(DetailAST ast) {
352         if (ast.getType() == TokenTypes.PACKAGE_DEF) {
353             samePackageDomainsRegExp = createSamePackageRegexp(
354                     samePackageMatchingDepth, ast);
355         }
356         else {
357             final String importFullPath = getFullImportIdent(ast);
358             final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
359             importToGroupList.add(new ImportDetails(importFullPath,
360                     getImportGroup(isStatic, importFullPath), isStatic, ast));
361         }
362     }
363 
364     @Override
365     public void finishTree(DetailAST rootAST) {
366         if (!importToGroupList.isEmpty()) {
367             finishImportList();
368         }
369     }
370 
371     /** Examine the order of all the imports and log any violations. */
372     private void finishImportList() {
373         String currentGroup = getFirstGroup();
374         int currentGroupNumber = customImportOrderRules.lastIndexOf(currentGroup);
375         ImportDetails previousImportObjectFromCurrentGroup = null;
376         String previousImportFromCurrentGroup = null;
377 
378         for (ImportDetails importObject : importToGroupList) {
379             final String importGroup = importObject.importGroup();
380             final String fullImportIdent = importObject.importFullPath();
381 
382             if (importGroup.equals(currentGroup)) {
383                 validateExtraEmptyLine(previousImportObjectFromCurrentGroup,
384                         importObject, fullImportIdent);
385                 if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) {
386                     log(importObject.importAST(), MSG_LEX,
387                             fullImportIdent, previousImportFromCurrentGroup);
388                 }
389                 else {
390                     previousImportFromCurrentGroup = fullImportIdent;
391                 }
392                 previousImportObjectFromCurrentGroup = importObject;
393             }
394             else {
395                 // not the last group, last one is always NON_GROUP
396                 if (customImportOrderRules.size() > currentGroupNumber + 1) {
397                     final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
398                     if (importGroup.equals(nextGroup)) {
399                         validateMissedEmptyLine(previousImportObjectFromCurrentGroup,
400                                 importObject, fullImportIdent);
401                         currentGroup = nextGroup;
402                         currentGroupNumber = customImportOrderRules.lastIndexOf(nextGroup);
403                         previousImportFromCurrentGroup = fullImportIdent;
404                     }
405                     else {
406                         logWrongImportGroupOrder(importObject.importAST(),
407                                 importGroup, nextGroup, fullImportIdent);
408                     }
409                     previousImportObjectFromCurrentGroup = importObject;
410                 }
411                 else {
412                     logWrongImportGroupOrder(importObject.importAST(),
413                             importGroup, currentGroup, fullImportIdent);
414                 }
415             }
416         }
417     }
418 
419     /**
420      * Log violation if empty line is missed.
421      *
422      * @param previousImport previous import from current group.
423      * @param importObject current import.
424      * @param fullImportIdent full import identifier.
425      */
426     private void validateMissedEmptyLine(ImportDetails previousImport,
427                                          ImportDetails importObject, String fullImportIdent) {
428         if (isEmptyLineMissed(previousImport, importObject)) {
429             log(importObject.importAST(), MSG_LINE_SEPARATOR, fullImportIdent);
430         }
431     }
432 
433     /**
434      * Log violation if extra empty line is present.
435      *
436      * @param previousImport previous import from current group.
437      * @param importObject current import.
438      * @param fullImportIdent full import identifier.
439      */
440     private void validateExtraEmptyLine(ImportDetails previousImport,
441                                         ImportDetails importObject, String fullImportIdent) {
442         if (isSeparatedByExtraEmptyLine(previousImport, importObject)) {
443             log(importObject.importAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent);
444         }
445     }
446 
447     /**
448      * Get first import group.
449      *
450      * @return
451      *        first import group of file.
452      */
453     private String getFirstGroup() {
454         final ImportDetails firstImport = importToGroupList.getFirst();
455         return getImportGroup(firstImport.staticImport(),
456                 firstImport.importFullPath());
457     }
458 
459     /**
460      * Examine alphabetical order of imports.
461      *
462      * @param previousImport
463      *        previous import of current group.
464      * @param currentImport
465      *        current import.
466      * @return
467      *        true, if previous and current import are not in alphabetical order.
468      */
469     private boolean isAlphabeticalOrderBroken(String previousImport,
470                                               String currentImport) {
471         return sortImportsInGroupAlphabetically
472                 && previousImport != null
473                 && compareImports(currentImport, previousImport) < 0;
474     }
475 
476     /**
477      * Examine empty lines between groups.
478      *
479      * @param previousImportObject
480      *        previous import in current group.
481      * @param currentImportObject
482      *        current import.
483      * @return
484      *        true, if current import NOT separated from previous import by empty line.
485      */
486     private boolean isEmptyLineMissed(ImportDetails previousImportObject,
487                                       ImportDetails currentImportObject) {
488         return separateLineBetweenGroups
489                 && getCountOfEmptyLinesBetween(
490                      previousImportObject.getEndLineNumber(),
491                      currentImportObject.getStartLineNumber()) != 1;
492     }
493 
494     /**
495      * Examine that imports separated by more than one empty line.
496      *
497      * @param previousImportObject
498      *        previous import in current group.
499      * @param currentImportObject
500      *        current import.
501      * @return
502      *        true, if current import separated from previous by more than one empty line.
503      */
504     private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject,
505                                                 ImportDetails currentImportObject) {
506         return previousImportObject != null
507                 && getCountOfEmptyLinesBetween(
508                      previousImportObject.getEndLineNumber(),
509                      currentImportObject.getStartLineNumber()) > 0;
510     }
511 
512     /**
513      * Log wrong import group order.
514      *
515      * @param importAST
516      *        import ast.
517      * @param importGroup
518      *        import group.
519      * @param currentGroupNumber
520      *        current group number we are checking.
521      * @param fullImportIdent
522      *        full import name.
523      */
524     private void logWrongImportGroupOrder(DetailAST importAST, String importGroup,
525             String currentGroupNumber, String fullImportIdent) {
526         if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
527             log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent);
528         }
529         else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
530             log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
531         }
532         else {
533             log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
534         }
535     }
536 
537     /**
538      * Get next import group.
539      *
540      * @param currentGroupNumber
541      *        current group number.
542      * @return
543      *        next import group.
544      */
545     private String getNextImportGroup(int currentGroupNumber) {
546         int nextGroupNumber = currentGroupNumber;
547 
548         while (customImportOrderRules.size() > nextGroupNumber + 1) {
549             if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
550                 break;
551             }
552             nextGroupNumber++;
553         }
554         return customImportOrderRules.get(nextGroupNumber);
555     }
556 
557     /**
558      * Checks if current group contains any import.
559      *
560      * @param currentGroup
561      *        current group.
562      * @return
563      *        true, if current group contains at least one import.
564      */
565     private boolean hasAnyImportInCurrentGroup(String currentGroup) {
566         boolean result = false;
567         for (ImportDetails currentImport : importToGroupList) {
568             if (currentGroup.equals(currentImport.importGroup())) {
569                 result = true;
570                 break;
571             }
572         }
573         return result;
574     }
575 
576     /**
577      * Get import valid group.
578      *
579      * @param isStatic
580      *        is static import.
581      * @param importPath
582      *        full import path.
583      * @return import valid group.
584      */
585     private String getImportGroup(boolean isStatic, String importPath) {
586         RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
587         if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
588             bestMatch.group = STATIC_RULE_GROUP;
589             bestMatch.matchLength = importPath.length();
590         }
591         else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
592             final String importPathTrimmedToSamePackageDepth =
593                     getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
594             if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
595                 bestMatch.group = SAME_PACKAGE_RULE_GROUP;
596                 bestMatch.matchLength = importPath.length();
597             }
598         }
599         for (String group : customImportOrderRules) {
600             if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
601                 bestMatch = findBetterPatternMatch(importPath,
602                         STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
603             }
604             if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
605                 bestMatch = findBetterPatternMatch(importPath,
606                         group, specialImportsRegExp, bestMatch);
607             }
608         }
609 
610         if (NON_GROUP_RULE_GROUP.equals(bestMatch.group)
611                 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
612                 && thirdPartyPackageRegExp.matcher(importPath).find()) {
613             bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
614         }
615         return bestMatch.group;
616     }
617 
618     /**
619      * Tries to find better matching regular expression:
620      * longer matching substring wins; in case of the same length,
621      * lower position of matching substring wins.
622      *
623      * @param importPath
624      *      Full import identifier
625      * @param group
626      *      Import group we are trying to assign the import
627      * @param regExp
628      *      Regular expression for import group
629      * @param currentBestMatch
630      *      object with currently best match
631      * @return better match (if found) or the same (currentBestMatch)
632      */
633     private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
634             Pattern regExp, RuleMatchForImport currentBestMatch) {
635         RuleMatchForImport betterMatchCandidate = currentBestMatch;
636         final Matcher matcher = regExp.matcher(importPath);
637         while (matcher.find()) {
638             final int matchStart = matcher.start();
639             final int length = matcher.end() - matchStart;
640             if (length > betterMatchCandidate.matchLength
641                     || length == betterMatchCandidate.matchLength
642                         && matchStart < betterMatchCandidate.matchPosition) {
643                 betterMatchCandidate = new RuleMatchForImport(group, length, matchStart);
644             }
645         }
646         return betterMatchCandidate;
647     }
648 
649     /**
650      * Checks compare two import paths.
651      *
652      * @param import1
653      *        current import.
654      * @param import2
655      *        previous import.
656      * @return a negative integer, zero, or a positive integer as the
657      *        specified String is greater than, equal to, or less
658      *        than this String, ignoring case considerations.
659      */
660     private static int compareImports(String import1, String import2) {
661         int result = 0;
662         final String separator = DOMAIN_SEPARATOR;
663         final String[] import1Tokens = import1.split(separator, -1);
664         final String[] import2Tokens = import2.split(separator, -1);
665         for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
666             final String import1Token = import1Tokens[i];
667             final String import2Token = import2Tokens[i];
668             result = import1Token.compareTo(import2Token);
669             if (result != 0) {
670                 break;
671             }
672         }
673         if (result == 0) {
674             result = Integer.compare(import1Tokens.length, import2Tokens.length);
675         }
676         return result;
677     }
678 
679     /**
680      * Counts empty lines between given parameters.
681      *
682      * @param fromLineNo
683      *        One-based line number of previous import.
684      * @param toLineNo
685      *        One-based line number of current import.
686      * @return count of empty lines between given parameters, exclusive,
687      *        eg., (fromLineNo, toLineNo).
688      */
689     private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) {
690         int result = 0;
691         final String[] lines = getLines();
692 
693         for (int i = fromLineNo + 1; i <= toLineNo - 1; i++) {
694             // "- 1" because the numbering is one-based
695             if (CommonUtil.isBlank(lines[i - 1])) {
696                 result++;
697             }
698         }
699         return result;
700     }
701 
702     /**
703      * Forms import full path.
704      *
705      * @param token
706      *        current token.
707      * @return full path or null.
708      */
709     private static String getFullImportIdent(DetailAST token) {
710         String ident = "";
711         if (token != null) {
712             ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
713         }
714         return ident;
715     }
716 
717     /**
718      * Parses ordering rule and adds it to the list with rules.
719      *
720      * @param ruleStr
721      *        String with rule.
722      * @throws IllegalArgumentException when SAME_PACKAGE rule parameter is not positive integer
723      * @throws IllegalStateException when ruleStr is unexpected value
724      */
725     private void addRulesToList(String ruleStr) {
726         if (STATIC_RULE_GROUP.equals(ruleStr)
727                 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
728                 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
729                 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
730             customImportOrderRules.add(ruleStr);
731         }
732         else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
733             final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
734                     ruleStr.indexOf(')'));
735             samePackageMatchingDepth = Integer.parseInt(rule);
736             if (samePackageMatchingDepth <= 0) {
737                 throw new IllegalArgumentException(
738                         "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
739             }
740             customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
741         }
742         else {
743             throw new IllegalStateException("Unexpected rule: " + ruleStr);
744         }
745     }
746 
747     /**
748      * Creates samePackageDomainsRegExp of the first package domains.
749      *
750      * @param firstPackageDomainsCount
751      *        number of first package domains.
752      * @param packageNode
753      *        package node.
754      * @return same package regexp.
755      */
756     private static String createSamePackageRegexp(int firstPackageDomainsCount,
757              DetailAST packageNode) {
758         final String packageFullPath = getFullImportIdent(packageNode);
759         return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
760     }
761 
762     /**
763      * Extracts defined amount of domains from the left side of package/import identifier.
764      *
765      * @param firstPackageDomainsCount
766      *        number of first package domains.
767      * @param packageFullPath
768      *        full identifier containing path to package or imported object.
769      * @return String with defined amount of domains or full identifier
770      *        (if full identifier had less domain than specified)
771      */
772     private static String getFirstDomainsFromIdent(
773             final int firstPackageDomainsCount, final String packageFullPath) {
774         final StringBuilder builder = new StringBuilder(256);
775         final String[] tokens = packageFullPath.split(DOMAIN_SEPARATOR, -1);
776         int count = firstPackageDomainsCount;
777 
778         for (String token : tokens) {
779             if (count <= 0) {
780                 break;
781             }
782             builder.append(token);
783             count--;
784         }
785         return builder.toString();
786     }
787 
788     /**
789      * Contains import attributes as line number, import full path, import
790      * group.
791      *
792      * @param importFullPath import full path
793      * @param importGroup import group
794      * @param staticImport if import is static
795      * @param importAST import AST
796      */
797     private record ImportDetails(
798             String importFullPath,
799             String importGroup,
800             boolean staticImport,
801             DetailAST importAST) {
802 
803         /**
804          * Get import start line number from ast.
805          *
806          * @return import start line from ast.
807          */
808         /* package */ int getStartLineNumber() {
809             return importAST.getLineNo();
810         }
811 
812         /**
813          * Get import end line number from ast.
814          *
815          * <p>
816          * <b>Note:</b> It can be different from <b>startLineNumber</b> when import statement span
817          * multiple lines.
818          * </p>
819          *
820          * @return import end line from ast.
821          */
822         /* package */ int getEndLineNumber() {
823             return importAST.getLastChild().getLineNo();
824         }
825     }
826 
827     /**
828      * Contains matching attributes assisting in definition of "best matching"
829      * group for import.
830      */
831     private static final class RuleMatchForImport {
832 
833         /** Position of matching string for current best match. */
834         private final int matchPosition;
835         /** Length of matching string for current best match. */
836         private int matchLength;
837         /** Import group for current best match. */
838         private String group;
839 
840         /**
841          * Constructor to initialize the fields.
842          *
843          * @param group
844          *        Matched group.
845          * @param length
846          *        Matching length.
847          * @param position
848          *        Matching position.
849          */
850         private RuleMatchForImport(String group, int length, int position) {
851             this.group = group;
852             matchLength = length;
853             matchPosition = position;
854         }
855 
856     }
857 
858 }