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 }