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 * {@snippet lang="text" : 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 * } 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 * {@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 155public class CustomImportOrderCheck extends AbstractCheck { 156 157 /** 158 * A key is pointing to the warning message text in "messages.properties" file. 159 */ 160 public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator"; 161 162 /** 163 * A key is pointing to the warning message text in "messages.properties" file. 164 */ 165 public static final String MSG_SEPARATED_IN_GROUP = "custom.import.order.separated.internally"; 166 167 /** 168 * A key is pointing to the warning message text in "messages.properties" file. 169 */ 170 public static final String MSG_LEX = "custom.import.order.lex"; 171 172 /** 173 * A key is pointing to the warning message text in "messages.properties" file. 174 */ 175 public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import"; 176 177 /** 178 * A key is pointing to the warning message text in "messages.properties" file. 179 */ 180 public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected"; 181 182 /** 183 * A key is pointing to the warning message text in "messages.properties" file. 184 */ 185 public static final String MSG_ORDER = "custom.import.order"; 186 187 /** STATIC group name. */ 188 public static final String STATIC_RULE_GROUP = "STATIC"; 189 190 /** SAME_PACKAGE group name. */ 191 public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE"; 192 193 /** THIRD_PARTY_PACKAGE group name. */ 194 public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE"; 195 196 /** STANDARD_JAVA_PACKAGE group name. */ 197 public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE"; 198 199 /** SPECIAL_IMPORTS group name. */ 200 public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS"; 201 202 /** NON_GROUP group name. */ 203 private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP"; 204 205 /** Pattern used to separate groups of imports. */ 206 private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*"); 207 208 /** Domain Separator. */ 209 private static final String DOMAIN_SEPARATOR = "\\."; 210 211 /** Specify ordered list of import groups. */ 212 private final List<String> customImportOrderRules = new ArrayList<>(); 213 214 /** Contains objects with import attributes. */ 215 private final List<ImportDetails> importToGroupList = new ArrayList<>(); 216 217 /** Specify RegExp for SAME_PACKAGE group imports. */ 218 private String samePackageDomainsRegExp = ""; 219 220 /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */ 221 private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\."); 222 223 /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */ 224 private Pattern thirdPartyPackageRegExp = Pattern.compile(".*"); 225 226 /** Specify RegExp for SPECIAL_IMPORTS group imports. */ 227 private Pattern specialImportsRegExp = Pattern.compile("^$"); 228 229 /** Force empty line separator between import groups. */ 230 private boolean separateLineBetweenGroups = true; 231 232 /** 233 * Force grouping alphabetically, 234 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>. 235 */ 236 private boolean sortImportsInGroupAlphabetically; 237 238 /** Number of first domains for SAME_PACKAGE group. */ 239 private int samePackageMatchingDepth; 240 241 /** 242 * Creates a new {@code CustomImportOrderCheck} instance. 243 */ 244 public CustomImportOrderCheck() { 245 // no code by default 246 } 247 248 /** 249 * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports. 250 * 251 * @param regexp 252 * user value. 253 * @since 5.8 254 */ 255 public final void setStandardPackageRegExp(Pattern regexp) { 256 standardPackageRegExp = regexp; 257 } 258 259 /** 260 * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports. 261 * 262 * @param regexp 263 * user value. 264 * @since 5.8 265 */ 266 public final void setThirdPartyPackageRegExp(Pattern regexp) { 267 thirdPartyPackageRegExp = regexp; 268 } 269 270 /** 271 * Setter to specify RegExp for SPECIAL_IMPORTS group imports. 272 * 273 * @param regexp 274 * user value. 275 * @since 5.8 276 */ 277 public final void setSpecialImportsRegExp(Pattern regexp) { 278 specialImportsRegExp = regexp; 279 } 280 281 /** 282 * Setter to force empty line separator between import groups. 283 * 284 * @param value 285 * user value. 286 * @since 5.8 287 */ 288 public final void setSeparateLineBetweenGroups(boolean value) { 289 separateLineBetweenGroups = value; 290 } 291 292 /** 293 * Setter to force grouping alphabetically, in 294 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 295 * 296 * @param value 297 * user value. 298 * @since 5.8 299 */ 300 public final void setSortImportsInGroupAlphabetically(boolean value) { 301 sortImportsInGroupAlphabetically = value; 302 } 303 304 /** 305 * Setter to specify ordered list of import groups. 306 * 307 * @param rules 308 * user value. 309 * @since 5.8 310 */ 311 public final void setCustomImportOrderRules(String... rules) { 312 Arrays.stream(rules) 313 .map(GROUP_SEPARATOR_PATTERN::split) 314 .flatMap(Arrays::stream) 315 .forEach(this::addRulesToList); 316 317 customImportOrderRules.add(NON_GROUP_RULE_GROUP); 318 } 319 320 @Override 321 public int[] getDefaultTokens() { 322 return getRequiredTokens(); 323 } 324 325 @Override 326 public int[] getAcceptableTokens() { 327 return getRequiredTokens(); 328 } 329 330 @Override 331 public int[] getRequiredTokens() { 332 return new int[] { 333 TokenTypes.IMPORT, 334 TokenTypes.STATIC_IMPORT, 335 TokenTypes.PACKAGE_DEF, 336 }; 337 } 338 339 @Override 340 public void beginTree(DetailAST rootAST) { 341 importToGroupList.clear(); 342 } 343 344 @Override 345 public void visitToken(DetailAST ast) { 346 if (ast.getType() == TokenTypes.PACKAGE_DEF) { 347 samePackageDomainsRegExp = createSamePackageRegexp( 348 samePackageMatchingDepth, ast); 349 } 350 else { 351 final String importFullPath = getFullImportIdent(ast); 352 final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT; 353 importToGroupList.add(new ImportDetails(importFullPath, 354 getImportGroup(isStatic, importFullPath), isStatic, ast)); 355 } 356 } 357 358 @Override 359 public void finishTree(DetailAST rootAST) { 360 if (!importToGroupList.isEmpty()) { 361 finishImportList(); 362 } 363 } 364 365 /** Examine the order of all the imports and log any violations. */ 366 private void finishImportList() { 367 String currentGroup = getFirstGroup(); 368 int currentGroupNumber = customImportOrderRules.lastIndexOf(currentGroup); 369 ImportDetails previousImportObjectFromCurrentGroup = null; 370 String previousImportFromCurrentGroup = null; 371 372 for (ImportDetails importObject : importToGroupList) { 373 final String importGroup = importObject.importGroup(); 374 final String fullImportIdent = importObject.importFullPath(); 375 376 if (importGroup.equals(currentGroup)) { 377 validateExtraEmptyLine(previousImportObjectFromCurrentGroup, 378 importObject, fullImportIdent); 379 if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) { 380 log(importObject.importAST(), MSG_LEX, 381 fullImportIdent, previousImportFromCurrentGroup); 382 } 383 else { 384 previousImportFromCurrentGroup = fullImportIdent; 385 } 386 previousImportObjectFromCurrentGroup = importObject; 387 } 388 else { 389 // not the last group, last one is always NON_GROUP 390 if (customImportOrderRules.size() > currentGroupNumber + 1) { 391 final String nextGroup = getNextImportGroup(currentGroupNumber + 1); 392 if (importGroup.equals(nextGroup)) { 393 validateMissedEmptyLine(previousImportObjectFromCurrentGroup, 394 importObject, fullImportIdent); 395 currentGroup = nextGroup; 396 currentGroupNumber = customImportOrderRules.lastIndexOf(nextGroup); 397 previousImportFromCurrentGroup = fullImportIdent; 398 } 399 else { 400 logWrongImportGroupOrder(importObject.importAST(), 401 importGroup, nextGroup, fullImportIdent); 402 } 403 previousImportObjectFromCurrentGroup = importObject; 404 } 405 else { 406 logWrongImportGroupOrder(importObject.importAST(), 407 importGroup, currentGroup, fullImportIdent); 408 } 409 } 410 } 411 } 412 413 /** 414 * Log violation if empty line is missed. 415 * 416 * @param previousImport previous import from current group. 417 * @param importObject current import. 418 * @param fullImportIdent full import identifier. 419 */ 420 private void validateMissedEmptyLine(ImportDetails previousImport, 421 ImportDetails importObject, String fullImportIdent) { 422 if (isEmptyLineMissed(previousImport, importObject)) { 423 log(importObject.importAST(), MSG_LINE_SEPARATOR, fullImportIdent); 424 } 425 } 426 427 /** 428 * Log violation if extra empty line is present. 429 * 430 * @param previousImport previous import from current group. 431 * @param importObject current import. 432 * @param fullImportIdent full import identifier. 433 */ 434 private void validateExtraEmptyLine(ImportDetails previousImport, 435 ImportDetails importObject, String fullImportIdent) { 436 if (isSeparatedByExtraEmptyLine(previousImport, importObject)) { 437 log(importObject.importAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent); 438 } 439 } 440 441 /** 442 * Get first import group. 443 * 444 * @return 445 * first import group of file. 446 */ 447 private String getFirstGroup() { 448 final ImportDetails firstImport = importToGroupList.getFirst(); 449 return getImportGroup(firstImport.staticImport(), 450 firstImport.importFullPath()); 451 } 452 453 /** 454 * Examine alphabetical order of imports. 455 * 456 * @param previousImport 457 * previous import of current group. 458 * @param currentImport 459 * current import. 460 * @return 461 * true, if previous and current import are not in alphabetical order. 462 */ 463 private boolean isAlphabeticalOrderBroken(String previousImport, 464 String currentImport) { 465 return sortImportsInGroupAlphabetically 466 && previousImport != null 467 && compareImports(currentImport, previousImport) < 0; 468 } 469 470 /** 471 * Examine empty lines between groups. 472 * 473 * @param previousImportObject 474 * previous import in current group. 475 * @param currentImportObject 476 * current import. 477 * @return 478 * true, if current import NOT separated from previous import by empty line. 479 */ 480 private boolean isEmptyLineMissed(ImportDetails previousImportObject, 481 ImportDetails currentImportObject) { 482 return separateLineBetweenGroups 483 && getCountOfEmptyLinesBetween( 484 previousImportObject.getEndLineNumber(), 485 currentImportObject.getStartLineNumber()) != 1; 486 } 487 488 /** 489 * Examine that imports separated by more than one empty line. 490 * 491 * @param previousImportObject 492 * previous import in current group. 493 * @param currentImportObject 494 * current import. 495 * @return 496 * true, if current import separated from previous by more than one empty line. 497 */ 498 private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject, 499 ImportDetails currentImportObject) { 500 return previousImportObject != null 501 && getCountOfEmptyLinesBetween( 502 previousImportObject.getEndLineNumber(), 503 currentImportObject.getStartLineNumber()) > 0; 504 } 505 506 /** 507 * Log wrong import group order. 508 * 509 * @param importAST 510 * import ast. 511 * @param importGroup 512 * import group. 513 * @param currentGroupNumber 514 * current group number we are checking. 515 * @param fullImportIdent 516 * full import name. 517 */ 518 private void logWrongImportGroupOrder(DetailAST importAST, String importGroup, 519 String currentGroupNumber, String fullImportIdent) { 520 if (NON_GROUP_RULE_GROUP.equals(importGroup)) { 521 log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent); 522 } 523 else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) { 524 log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent); 525 } 526 else { 527 log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent); 528 } 529 } 530 531 /** 532 * Get next import group. 533 * 534 * @param currentGroupNumber 535 * current group number. 536 * @return 537 * next import group. 538 */ 539 private String getNextImportGroup(int currentGroupNumber) { 540 int nextGroupNumber = currentGroupNumber; 541 542 while (customImportOrderRules.size() > nextGroupNumber + 1) { 543 if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) { 544 break; 545 } 546 nextGroupNumber++; 547 } 548 return customImportOrderRules.get(nextGroupNumber); 549 } 550 551 /** 552 * Checks if current group contains any import. 553 * 554 * @param currentGroup 555 * current group. 556 * @return 557 * true, if current group contains at least one import. 558 */ 559 private boolean hasAnyImportInCurrentGroup(String currentGroup) { 560 boolean result = false; 561 for (ImportDetails currentImport : importToGroupList) { 562 if (currentGroup.equals(currentImport.importGroup())) { 563 result = true; 564 break; 565 } 566 } 567 return result; 568 } 569 570 /** 571 * Get import valid group. 572 * 573 * @param isStatic 574 * is static import. 575 * @param importPath 576 * full import path. 577 * @return import valid group. 578 */ 579 private String getImportGroup(boolean isStatic, String importPath) { 580 RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0); 581 if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) { 582 bestMatch.group = STATIC_RULE_GROUP; 583 bestMatch.matchLength = importPath.length(); 584 } 585 else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 586 final String importPathTrimmedToSamePackageDepth = 587 getFirstDomainsFromIdent(samePackageMatchingDepth, importPath); 588 if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) { 589 bestMatch.group = SAME_PACKAGE_RULE_GROUP; 590 bestMatch.matchLength = importPath.length(); 591 } 592 } 593 for (String group : customImportOrderRules) { 594 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) { 595 bestMatch = findBetterPatternMatch(importPath, 596 STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch); 597 } 598 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) { 599 bestMatch = findBetterPatternMatch(importPath, 600 group, specialImportsRegExp, bestMatch); 601 } 602 } 603 604 if (NON_GROUP_RULE_GROUP.equals(bestMatch.group) 605 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP) 606 && thirdPartyPackageRegExp.matcher(importPath).find()) { 607 bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP; 608 } 609 return bestMatch.group; 610 } 611 612 /** 613 * Tries to find better matching regular expression: 614 * longer matching substring wins; in case of the same length, 615 * lower position of matching substring wins. 616 * 617 * @param importPath 618 * Full import identifier 619 * @param group 620 * Import group we are trying to assign the import 621 * @param regExp 622 * Regular expression for import group 623 * @param currentBestMatch 624 * object with currently best match 625 * @return better match (if found) or the same (currentBestMatch) 626 */ 627 private static RuleMatchForImport findBetterPatternMatch(String importPath, String group, 628 Pattern regExp, RuleMatchForImport currentBestMatch) { 629 RuleMatchForImport betterMatchCandidate = currentBestMatch; 630 final Matcher matcher = regExp.matcher(importPath); 631 while (matcher.find()) { 632 final int matchStart = matcher.start(); 633 final int length = matcher.end() - matchStart; 634 if (length > betterMatchCandidate.matchLength 635 || length == betterMatchCandidate.matchLength 636 && matchStart < betterMatchCandidate.matchPosition) { 637 betterMatchCandidate = new RuleMatchForImport(group, length, matchStart); 638 } 639 } 640 return betterMatchCandidate; 641 } 642 643 /** 644 * Checks compare two import paths. 645 * 646 * @param import1 647 * current import. 648 * @param import2 649 * previous import. 650 * @return a negative integer, zero, or a positive integer as the 651 * specified String is greater than, equal to, or less 652 * than this String, ignoring case considerations. 653 */ 654 private static int compareImports(String import1, String import2) { 655 int result = 0; 656 final String separator = DOMAIN_SEPARATOR; 657 final String[] import1Tokens = import1.split(separator, -1); 658 final String[] import2Tokens = import2.split(separator, -1); 659 for (int index = 0; 660 index != import1Tokens.length && index != import2Tokens.length; 661 index++) { 662 final String import1Token = import1Tokens[index]; 663 final String import2Token = import2Tokens[index]; 664 result = import1Token.compareTo(import2Token); 665 if (result != 0) { 666 break; 667 } 668 } 669 if (result == 0) { 670 result = Integer.compare(import1Tokens.length, import2Tokens.length); 671 } 672 return result; 673 } 674 675 /** 676 * Counts empty lines between given parameters. 677 * 678 * @param fromLineNo 679 * One-based line number of previous import. 680 * @param toLineNo 681 * One-based line number of current import. 682 * @return count of empty lines between given parameters, exclusive, 683 * eg., (fromLineNo, toLineNo). 684 */ 685 private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) { 686 int result = 0; 687 final String[] lines = getLines(); 688 689 for (int index = fromLineNo + 1; index <= toLineNo - 1; index++) { 690 // "- 1" because the numbering is one-based 691 if (CommonUtil.isBlank(lines[index - 1])) { 692 result++; 693 } 694 } 695 return result; 696 } 697 698 /** 699 * Forms import full path. 700 * 701 * @param token 702 * current token. 703 * @return full path or null. 704 */ 705 private static String getFullImportIdent(DetailAST token) { 706 String ident = ""; 707 if (token != null) { 708 ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText(); 709 } 710 return ident; 711 } 712 713 /** 714 * Parses ordering rule and adds it to the list with rules. 715 * 716 * @param ruleStr 717 * String with rule. 718 * @throws IllegalArgumentException when SAME_PACKAGE rule parameter is not positive integer 719 * @throws IllegalStateException when ruleStr is unexpected value 720 */ 721 private void addRulesToList(String ruleStr) { 722 if (STATIC_RULE_GROUP.equals(ruleStr) 723 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr) 724 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr) 725 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) { 726 customImportOrderRules.add(ruleStr); 727 } 728 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) { 729 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1, 730 ruleStr.indexOf(')')); 731 samePackageMatchingDepth = Integer.parseInt(rule); 732 if (samePackageMatchingDepth <= 0) { 733 throw new IllegalArgumentException( 734 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr); 735 } 736 customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP); 737 } 738 else { 739 throw new IllegalStateException("Unexpected rule: " + ruleStr); 740 } 741 } 742 743 /** 744 * Creates samePackageDomainsRegExp of the first package domains. 745 * 746 * @param firstPackageDomainsCount 747 * number of first package domains. 748 * @param packageNode 749 * package node. 750 * @return same package regexp. 751 */ 752 private static String createSamePackageRegexp(int firstPackageDomainsCount, 753 DetailAST packageNode) { 754 final String packageFullPath = getFullImportIdent(packageNode); 755 return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath); 756 } 757 758 /** 759 * Extracts defined amount of domains from the left side of package/import identifier. 760 * 761 * @param firstPackageDomainsCount 762 * number of first package domains. 763 * @param packageFullPath 764 * full identifier containing path to package or imported object. 765 * @return String with defined amount of domains or full identifier 766 * (if full identifier had less domain than specified) 767 */ 768 private static String getFirstDomainsFromIdent( 769 final int firstPackageDomainsCount, final String packageFullPath) { 770 final StringBuilder builder = new StringBuilder(256); 771 final String[] tokens = packageFullPath.split(DOMAIN_SEPARATOR, -1); 772 int count = firstPackageDomainsCount; 773 774 for (String token : tokens) { 775 if (count <= 0) { 776 break; 777 } 778 builder.append(token); 779 count--; 780 } 781 return builder.toString(); 782 } 783 784 /** 785 * Contains import attributes as line number, import full path, import group. 786 * 787 * @param importFullPath import full path 788 * @param importGroup import group 789 * @param staticImport if import is static 790 * @param importAST import AST 791 */ 792 private record ImportDetails( 793 String importFullPath, 794 String importGroup, 795 boolean staticImport, 796 DetailAST importAST) { 797 798 /** 799 * Get import start line number from ast. 800 * 801 * @return import start line from ast. 802 */ 803 /* package */ int getStartLineNumber() { 804 return importAST.getLineNo(); 805 } 806 807 /** 808 * Get import end line number from ast. 809 * 810 * <p> 811 * <b>Note:</b> It can be different from <b>startLineNumber</b> when import statement span 812 * multiple lines. 813 * </p> 814 * 815 * @return import end line from ast. 816 */ 817 /* package */ int getEndLineNumber() { 818 return importAST.getLastChild().getLineNo(); 819 } 820 } 821 822 /** 823 * Contains matching attributes assisting in definition of "best matching" group for import. 824 */ 825 private static final class RuleMatchForImport { 826 827 /** Position of matching string for current best match. */ 828 private final int matchPosition; 829 /** Length of matching string for current best match. */ 830 private int matchLength; 831 /** Import group for current best match. */ 832 private String group; 833 834 /** 835 * Constructor to initialize the fields. 836 * 837 * @param group 838 * Matched group. 839 * @param length 840 * Matching length. 841 * @param position 842 * Matching position. 843 */ 844 private RuleMatchForImport(String group, int length, int position) { 845 this.group = group; 846 matchLength = length; 847 matchPosition = position; 848 } 849 850 } 851 852}