001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.imports; 021 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.List; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 029import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.FullIdent; 032import com.puppycrawl.tools.checkstyle.api.TokenTypes; 033import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 034 035/** 036 * <div> 037 * Checks that the groups of import declarations appear in the order specified 038 * by the user. If there is an import but its group is not specified in the 039 * configuration such an import should be placed at the end of the import list. 040 * </div> 041 * 042 * <p> 043 * The rule consists of: 044 * </p> 045 * <ol> 046 * <li> 047 * STATIC group. This group sets the ordering of static imports. 048 * </li> 049 * <li> 050 * SAME_PACKAGE(n) group. This group sets the ordering of the same package imports. 051 * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package 052 * name and import name are identical: 053 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 054 * package java.util.concurrent.locks; 055 * 056 * import java.io.File; 057 * import java.util.*; //#1 058 * import java.util.List; //#2 059 * import java.util.StringTokenizer; //#3 060 * import java.util.concurrent.*; //#4 061 * import java.util.concurrent.AbstractExecutorService; //#5 062 * import java.util.concurrent.locks.LockSupport; //#6 063 * import java.util.regex.Pattern; //#7 064 * import java.util.regex.Matcher; //#8 065 * </code></pre></div> 066 * If we have SAME_PACKAGE(3) on configuration file, imports #4-6 will be considered as 067 * a SAME_PACKAGE group (java.util.concurrent.*, java.util.concurrent.AbstractExecutorService, 068 * java.util.concurrent.locks.LockSupport). SAME_PACKAGE(2) will include #1-8. 069 * SAME_PACKAGE(4) will include only #6. SAME_PACKAGE(5) will result in no imports assigned 070 * to SAME_PACKAGE group because actual package java.util.concurrent.locks has only 4 domains. 071 * </li> 072 * <li> 073 * THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports. 074 * Third party imports are all imports except STATIC, SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and 075 * SPECIAL_IMPORTS. 076 * </li> 077 * <li> 078 * STANDARD_JAVA_PACKAGE group. By default, this group sets ordering of standard java/javax imports. 079 * </li> 080 * <li> 081 * SPECIAL_IMPORTS group. This group may contain some imports that have particular meaning for the 082 * user. 083 * </li> 084 * </ol> 085 * 086 * <p> 087 * Notes: 088 * Rules are configured as a comma-separated ordered list. 089 * </p> 090 * 091 * <p> 092 * Note: '###' group separator is deprecated (in favor of a comma-separated list), 093 * but is currently supported for backward compatibility. 094 * </p> 095 * 096 * <p> 097 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use 098 * thirdPartyPackageRegExp and standardPackageRegExp options. 099 * </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 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 131 * import com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck; 132 * import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck; 133 * </code></pre></div> 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 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 144 * import com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck; 145 * </code></pre></div> 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 155public 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}