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