001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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.List; 024import java.util.StringTokenizer; 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 * <p> 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 * </p> 041 * <p> 042 * The rule consists of: 043 * </p> 044 * <ol> 045 * <li> 046 * STATIC group. This group sets the ordering of static imports. 047 * </li> 048 * <li> 049 * SAME_PACKAGE(n) group. This group sets the ordering of the same package imports. 050 * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package 051 * name and import name are identical: 052 * <pre> 053 * package java.util.concurrent.locks; 054 * 055 * import java.io.File; 056 * import java.util.*; //#1 057 * import java.util.List; //#2 058 * import java.util.StringTokenizer; //#3 059 * import java.util.concurrent.*; //#4 060 * import java.util.concurrent.AbstractExecutorService; //#5 061 * import java.util.concurrent.locks.LockSupport; //#6 062 * import java.util.regex.Pattern; //#7 063 * import java.util.regex.Matcher; //#8 064 * </pre> 065 * If we have SAME_PACKAGE(3) on configuration file, imports #4-6 will be considered as 066 * a SAME_PACKAGE group (java.util.concurrent.*, java.util.concurrent.AbstractExecutorService, 067 * java.util.concurrent.locks.LockSupport). SAME_PACKAGE(2) will include #1-8. 068 * SAME_PACKAGE(4) will include only #6. SAME_PACKAGE(5) will result in no imports assigned 069 * to SAME_PACKAGE group because actual package java.util.concurrent.locks has only 4 domains. 070 * </li> 071 * <li> 072 * THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports. 073 * Third party imports are all imports except STATIC, SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and 074 * SPECIAL_IMPORTS. 075 * </li> 076 * <li> 077 * STANDARD_JAVA_PACKAGE group. By default this group sets ordering of standard java/javax imports. 078 * </li> 079 * <li> 080 * SPECIAL_IMPORTS group. This group may contains some imports that have particular meaning for the 081 * user. 082 * </li> 083 * </ol> 084 * <p> 085 * Use the separator '###' between rules. 086 * </p> 087 * <p> 088 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use 089 * thirdPartyPackageRegExp and standardPackageRegExp options. 090 * </p> 091 * <p> 092 * Pretty often one import can match more than one group. For example, static import from standard 093 * package or regular expressions are configured to allow one import match multiple groups. 094 * In this case, group will be assigned according to priorities: 095 * </p> 096 * <ol> 097 * <li> 098 * STATIC has top priority 099 * </li> 100 * <li> 101 * SAME_PACKAGE has second priority 102 * </li> 103 * <li> 104 * STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS will compete using "best match" rule: longer 105 * matching substring wins; in case of the same length, lower position of matching substring 106 * wins; if position is the same, order of rules in configuration solves the puzzle. 107 * </li> 108 * <li> 109 * THIRD_PARTY has the least priority 110 * </li> 111 * </ol> 112 * <p> 113 * Few examples to illustrate "best match": 114 * </p> 115 * <p> 116 * 1. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="ImportOrderCheck" and input file: 117 * </p> 118 * <pre> 119 * import com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck; 120 * import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck; 121 * </pre> 122 * <p> 123 * Result: imports will be assigned to SPECIAL_IMPORTS, because matching substring length is 16. 124 * Matching substring for STANDARD_JAVA_PACKAGE is 5. 125 * </p> 126 * <p> 127 * 2. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="Avoid" and file: 128 * </p> 129 * <pre> 130 * import com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck; 131 * </pre> 132 * <p> 133 * Result: import will be assigned to SPECIAL_IMPORTS. Matching substring length is 5 for both 134 * patterns. However, "Avoid" position is lower than "Check" position. 135 * </p> 136 * <ul> 137 * <li> 138 * Property {@code customImportOrderRules} - Specify format of order declaration 139 * customizing by user. Default value is {@code ""}. 140 * </li> 141 * <li> 142 * Property {@code standardPackageRegExp} - Specify RegExp for STANDARD_JAVA_PACKAGE group imports. 143 * Default value is {@code "^(java|javax)\."}. 144 * </li> 145 * <li> 146 * Property {@code thirdPartyPackageRegExp} - Specify RegExp for THIRD_PARTY_PACKAGE group imports. 147 * Default value is {@code ".*"}. 148 * </li> 149 * <li> 150 * Property {@code specialImportsRegExp} - Specify RegExp for SPECIAL_IMPORTS group imports. 151 * Default value is {@code "^$" (empty)}. 152 * </li> 153 * <li> 154 * Property {@code separateLineBetweenGroups} - Force empty line separator between 155 * import groups. 156 * Default value is {@code true}. 157 * </li> 158 * <li> 159 * Property {@code sortImportsInGroupAlphabetically} - Force grouping alphabetically, 160 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 161 * Default value is {@code false}. 162 * </li> 163 * </ul> 164 * <p> 165 * To configure the check so that it matches default Eclipse formatter configuration 166 * (tested on Kepler and Luna releases): 167 * </p> 168 * <ul> 169 * <li> 170 * group of static imports is on the top 171 * </li> 172 * <li> 173 * groups of non-static imports: "java" and "javax" packages first, then "org" and then all other 174 * imports 175 * </li> 176 * <li> 177 * imports will be sorted in the groups 178 * </li> 179 * <li> 180 * groups are separated by single blank line 181 * </li> 182 * </ul> 183 * <p> 184 * Notes: 185 * </p> 186 * <ul> 187 * <li> 188 * "com" package is not mentioned on configuration, because it is ignored by Eclipse Kepler and Luna 189 * (looks like Eclipse defect) 190 * </li> 191 * <li> 192 * configuration below doesn't work in all 100% cases due to inconsistent behavior prior to Mars 193 * release, but covers most scenarios 194 * </li> 195 * </ul> 196 * <pre> 197 * <module name="CustomImportOrder"> 198 * <property name="customImportOrderRules" 199 * value="STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS"/> 200 * <property name="specialImportsRegExp" value="^org\."/> 201 * <property name="sortImportsInGroupAlphabetically" value="true"/> 202 * <property name="separateLineBetweenGroups" value="true"/> 203 * </module> 204 * </pre> 205 * <p> 206 * To configure the check so that it matches default Eclipse formatter configuration 207 * (tested on Mars release): 208 * </p> 209 * <ul> 210 * <li> 211 * group of static imports is on the top 212 * </li> 213 * <li> 214 * groups of non-static imports: "java" and "javax" packages first, then "org" and "com", 215 * then all other imports as one group 216 * </li> 217 * <li> 218 * imports will be sorted in the groups 219 * </li> 220 * <li> 221 * groups are separated by one blank line 222 * </li> 223 * </ul> 224 * <pre> 225 * <module name="CustomImportOrder"> 226 * <property name="customImportOrderRules" 227 * value="STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS###THIRD_PARTY_PACKAGE"/> 228 * <property name="specialImportsRegExp" value="^org\."/> 229 * <property name="thirdPartyPackageRegExp" value="^com\."/> 230 * <property name="sortImportsInGroupAlphabetically" value="true"/> 231 * <property name="separateLineBetweenGroups" value="true"/> 232 * </module> 233 * </pre> 234 * <p> 235 * To configure the check so that it matches default IntelliJ IDEA formatter configuration 236 * (tested on v14): 237 * </p> 238 * <ul> 239 * <li> 240 * group of static imports is on the bottom 241 * </li> 242 * <li> 243 * groups of non-static imports: all imports except of "javax" and "java", then "javax" and "java" 244 * </li> 245 * <li> 246 * imports will be sorted in the groups 247 * </li> 248 * <li> 249 * groups are separated by one blank line 250 * </li> 251 * </ul> 252 * <p> 253 * Note: "separated" option is disabled because IDEA default has blank line between "java" and 254 * static imports, and no blank line between "javax" and "java" 255 * </p> 256 * <pre> 257 * <module name="CustomImportOrder"> 258 * <property name="customImportOrderRules" 259 * value="THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE###STATIC"/> 260 * <property name="specialImportsRegExp" value="^javax\."/> 261 * <property name="standardPackageRegExp" value="^java\."/> 262 * <property name="sortImportsInGroupAlphabetically" value="true"/> 263 * <property name="separateLineBetweenGroups" value="false"/> 264 * </module> 265 * </pre> 266 * <p> 267 * To configure the check so that it matches default NetBeans formatter configuration 268 * (tested on v8): 269 * </p> 270 * <ul> 271 * <li> 272 * groups of non-static imports are not defined, all imports will be sorted as a one group 273 * </li> 274 * <li> 275 * static imports are not separated, they will be sorted along with other imports 276 * </li> 277 * </ul> 278 * <pre> 279 * <module name="CustomImportOrder"/> 280 * </pre> 281 * <p> 282 * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use 283 * thirdPartyPackageRegExp and standardPackageRegExp options. 284 * </p> 285 * <pre> 286 * <module name="CustomImportOrder"> 287 * <property name="customImportOrderRules" 288 * value="STATIC###SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE"/> 289 * <property name="thirdPartyPackageRegExp" value="^(com|org)\."/> 290 * <property name="standardPackageRegExp" value="^(java|javax)\."/> 291 * </module> 292 * </pre> 293 * <p> 294 * Also, this check can be configured to force empty line separator between 295 * import groups. For example. 296 * </p> 297 * <pre> 298 * <module name="CustomImportOrder"> 299 * <property name="separateLineBetweenGroups" value="true"/> 300 * </module> 301 * </pre> 302 * <p> 303 * It is possible to enforce 304 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a> 305 * of imports in groups using the following configuration: 306 * </p> 307 * <pre> 308 * <module name="CustomImportOrder"> 309 * <property name="sortImportsInGroupAlphabetically" value="true"/> 310 * </module> 311 * </pre> 312 * <p> 313 * Example of ASCII order: 314 * </p> 315 * <pre> 316 * import java.awt.Dialog; 317 * import java.awt.Window; 318 * import java.awt.color.ColorSpace; 319 * import java.awt.Frame; // violation here - in ASCII order 'F' should go before 'c', 320 * // as all uppercase come before lowercase letters 321 * </pre> 322 * <p> 323 * To force checking imports sequence such as: 324 * </p> 325 * <pre> 326 * package com.puppycrawl.tools.checkstyle.imports; 327 * 328 * import com.google.common.annotations.GwtCompatible; 329 * import com.google.common.annotations.Beta; 330 * import com.google.common.annotations.VisibleForTesting; 331 * 332 * import org.abego.treelayout.Configuration; 333 * 334 * import static sun.tools.util.ModifierFilter.ALL_ACCESS; 335 * 336 * import com.google.common.annotations.GwtCompatible; // violation here - should be in the 337 * // THIRD_PARTY_PACKAGE group 338 * import android.*; 339 * </pre> 340 * <p> 341 * configure as follows: 342 * </p> 343 * <pre> 344 * <module name="CustomImportOrder"> 345 * <property name="customImportOrderRules" 346 * value="SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STATIC###SPECIAL_IMPORTS"/> 347 * <property name="specialImportsRegExp" value="^android\."/> 348 * </module> 349 * </pre> 350 * 351 * @since 5.8 352 */ 353@FileStatefulCheck 354public class CustomImportOrderCheck extends AbstractCheck { 355 356 /** 357 * A key is pointing to the warning message text in "messages.properties" 358 * file. 359 */ 360 public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator"; 361 362 /** 363 * A key is pointing to the warning message text in "messages.properties" 364 * file. 365 */ 366 public static final String MSG_SEPARATED_IN_GROUP = "custom.import.order.separated.internally"; 367 368 /** 369 * A key is pointing to the warning message text in "messages.properties" 370 * file. 371 */ 372 public static final String MSG_LEX = "custom.import.order.lex"; 373 374 /** 375 * A key is pointing to the warning message text in "messages.properties" 376 * file. 377 */ 378 public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import"; 379 380 /** 381 * A key is pointing to the warning message text in "messages.properties" 382 * file. 383 */ 384 public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected"; 385 386 /** 387 * A key is pointing to the warning message text in "messages.properties" 388 * file. 389 */ 390 public static final String MSG_ORDER = "custom.import.order"; 391 392 /** STATIC group name. */ 393 public static final String STATIC_RULE_GROUP = "STATIC"; 394 395 /** SAME_PACKAGE group name. */ 396 public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE"; 397 398 /** THIRD_PARTY_PACKAGE group name. */ 399 public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE"; 400 401 /** STANDARD_JAVA_PACKAGE group name. */ 402 public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE"; 403 404 /** SPECIAL_IMPORTS group name. */ 405 public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS"; 406 407 /** NON_GROUP group name. */ 408 private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP"; 409 410 /** Pattern used to separate groups of imports. */ 411 private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*"); 412 413 /** Processed list of import order rules. */ 414 private final List<String> customOrderRules = new ArrayList<>(); 415 416 /** Contains objects with import attributes. */ 417 private final List<ImportDetails> importToGroupList = new ArrayList<>(); 418 419 /** Specify format of order declaration customizing by user. */ 420 private String customImportOrderRules = ""; 421 422 /** Specify RegExp for SAME_PACKAGE group imports. */ 423 private String samePackageDomainsRegExp = ""; 424 425 /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */ 426 private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\."); 427 428 /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */ 429 private Pattern thirdPartyPackageRegExp = Pattern.compile(".*"); 430 431 /** Specify RegExp for SPECIAL_IMPORTS group imports. */ 432 private Pattern specialImportsRegExp = Pattern.compile("^$"); 433 434 /** Force empty line separator between import groups. */ 435 private boolean separateLineBetweenGroups = true; 436 437 /** 438 * Force grouping alphabetically, 439 * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>. 440 */ 441 private boolean sortImportsInGroupAlphabetically; 442 443 /** Number of first domains for SAME_PACKAGE group. */ 444 private int samePackageMatchingDepth = 2; 445 446 /** 447 * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports. 448 * 449 * @param regexp 450 * user value. 451 */ 452 public final void setStandardPackageRegExp(Pattern regexp) { 453 standardPackageRegExp = regexp; 454 } 455 456 /** 457 * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports. 458 * 459 * @param regexp 460 * user value. 461 */ 462 public final void setThirdPartyPackageRegExp(Pattern regexp) { 463 thirdPartyPackageRegExp = regexp; 464 } 465 466 /** 467 * Setter to specify RegExp for SPECIAL_IMPORTS group imports. 468 * 469 * @param regexp 470 * user value. 471 */ 472 public final void setSpecialImportsRegExp(Pattern regexp) { 473 specialImportsRegExp = regexp; 474 } 475 476 /** 477 * Setter to force empty line separator between import groups. 478 * 479 * @param value 480 * user value. 481 */ 482 public final void setSeparateLineBetweenGroups(boolean value) { 483 separateLineBetweenGroups = value; 484 } 485 486 /** 487 * Setter to force grouping alphabetically, in 488 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>. 489 * 490 * @param value 491 * user value. 492 */ 493 public final void setSortImportsInGroupAlphabetically(boolean value) { 494 sortImportsInGroupAlphabetically = value; 495 } 496 497 /** 498 * Setter to specify format of order declaration customizing by user. 499 * 500 * @param inputCustomImportOrder 501 * user value. 502 */ 503 public final void setCustomImportOrderRules(final String inputCustomImportOrder) { 504 if (!customImportOrderRules.equals(inputCustomImportOrder)) { 505 for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) { 506 addRulesToList(currentState); 507 } 508 customOrderRules.add(NON_GROUP_RULE_GROUP); 509 } 510 customImportOrderRules = inputCustomImportOrder; 511 } 512 513 @Override 514 public int[] getDefaultTokens() { 515 return getRequiredTokens(); 516 } 517 518 @Override 519 public int[] getAcceptableTokens() { 520 return getRequiredTokens(); 521 } 522 523 @Override 524 public int[] getRequiredTokens() { 525 return new int[] { 526 TokenTypes.IMPORT, 527 TokenTypes.STATIC_IMPORT, 528 TokenTypes.PACKAGE_DEF, 529 }; 530 } 531 532 @Override 533 public void beginTree(DetailAST rootAST) { 534 importToGroupList.clear(); 535 } 536 537 @Override 538 public void visitToken(DetailAST ast) { 539 if (ast.getType() == TokenTypes.PACKAGE_DEF) { 540 samePackageDomainsRegExp = createSamePackageRegexp( 541 samePackageMatchingDepth, ast); 542 } 543 else { 544 final String importFullPath = getFullImportIdent(ast); 545 final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT; 546 importToGroupList.add(new ImportDetails(importFullPath, 547 getImportGroup(isStatic, importFullPath), isStatic, ast)); 548 } 549 } 550 551 @Override 552 public void finishTree(DetailAST rootAST) { 553 if (!importToGroupList.isEmpty()) { 554 finishImportList(); 555 } 556 } 557 558 /** Examine the order of all the imports and log any violations. */ 559 private void finishImportList() { 560 String currentGroup = getFirstGroup(); 561 int currentGroupNumber = customOrderRules.indexOf(currentGroup); 562 ImportDetails previousImportObjectFromCurrentGroup = null; 563 String previousImportFromCurrentGroup = null; 564 565 for (ImportDetails importObject : importToGroupList) { 566 final String importGroup = importObject.getImportGroup(); 567 final String fullImportIdent = importObject.getImportFullPath(); 568 569 if (importGroup.equals(currentGroup)) { 570 validateExtraEmptyLine(previousImportObjectFromCurrentGroup, 571 importObject, fullImportIdent); 572 if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) { 573 log(importObject.getImportAST(), MSG_LEX, 574 fullImportIdent, previousImportFromCurrentGroup); 575 } 576 else { 577 previousImportFromCurrentGroup = fullImportIdent; 578 } 579 previousImportObjectFromCurrentGroup = importObject; 580 } 581 else { 582 // not the last group, last one is always NON_GROUP 583 if (customOrderRules.size() > currentGroupNumber + 1) { 584 final String nextGroup = getNextImportGroup(currentGroupNumber + 1); 585 if (importGroup.equals(nextGroup)) { 586 validateMissedEmptyLine(previousImportObjectFromCurrentGroup, 587 importObject, fullImportIdent); 588 currentGroup = nextGroup; 589 currentGroupNumber = customOrderRules.indexOf(nextGroup); 590 previousImportFromCurrentGroup = fullImportIdent; 591 } 592 else { 593 logWrongImportGroupOrder(importObject.getImportAST(), 594 importGroup, nextGroup, fullImportIdent); 595 } 596 previousImportObjectFromCurrentGroup = importObject; 597 } 598 else { 599 logWrongImportGroupOrder(importObject.getImportAST(), 600 importGroup, currentGroup, fullImportIdent); 601 } 602 } 603 } 604 } 605 606 /** 607 * Log violation if empty line is missed. 608 * 609 * @param previousImport previous import from current group. 610 * @param importObject current import. 611 * @param fullImportIdent full import identifier. 612 */ 613 private void validateMissedEmptyLine(ImportDetails previousImport, 614 ImportDetails importObject, String fullImportIdent) { 615 if (isEmptyLineMissed(previousImport, importObject)) { 616 log(importObject.getImportAST(), MSG_LINE_SEPARATOR, fullImportIdent); 617 } 618 } 619 620 /** 621 * Log violation if extra empty line is present. 622 * 623 * @param previousImport previous import from current group. 624 * @param importObject current import. 625 * @param fullImportIdent full import identifier. 626 */ 627 private void validateExtraEmptyLine(ImportDetails previousImport, 628 ImportDetails importObject, String fullImportIdent) { 629 if (isSeparatedByExtraEmptyLine(previousImport, importObject)) { 630 log(importObject.getImportAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent); 631 } 632 } 633 634 /** 635 * Get first import group. 636 * 637 * @return 638 * first import group of file. 639 */ 640 private String getFirstGroup() { 641 final ImportDetails firstImport = importToGroupList.get(0); 642 return getImportGroup(firstImport.isStaticImport(), 643 firstImport.getImportFullPath()); 644 } 645 646 /** 647 * Examine alphabetical order of imports. 648 * 649 * @param previousImport 650 * previous import of current group. 651 * @param currentImport 652 * current import. 653 * @return 654 * true, if previous and current import are not in alphabetical order. 655 */ 656 private boolean isAlphabeticalOrderBroken(String previousImport, 657 String currentImport) { 658 return sortImportsInGroupAlphabetically 659 && previousImport != null 660 && compareImports(currentImport, previousImport) < 0; 661 } 662 663 /** 664 * Examine empty lines between groups. 665 * 666 * @param previousImportObject 667 * previous import in current group. 668 * @param currentImportObject 669 * current import. 670 * @return 671 * true, if current import NOT separated from previous import by empty line. 672 */ 673 private boolean isEmptyLineMissed(ImportDetails previousImportObject, 674 ImportDetails currentImportObject) { 675 return separateLineBetweenGroups 676 && getCountOfEmptyLinesBetween( 677 previousImportObject.getEndLineNumber(), 678 currentImportObject.getStartLineNumber()) != 1; 679 } 680 681 /** 682 * Examine that imports separated by more than one empty line. 683 * 684 * @param previousImportObject 685 * previous import in current group. 686 * @param currentImportObject 687 * current import. 688 * @return 689 * true, if current import separated from previous by more that one empty line. 690 */ 691 private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject, 692 ImportDetails currentImportObject) { 693 return previousImportObject != null 694 && getCountOfEmptyLinesBetween( 695 previousImportObject.getEndLineNumber(), 696 currentImportObject.getStartLineNumber()) > 0; 697 } 698 699 /** 700 * Log wrong import group order. 701 * 702 * @param importAST 703 * import ast. 704 * @param importGroup 705 * import group. 706 * @param currentGroupNumber 707 * current group number we are checking. 708 * @param fullImportIdent 709 * full import name. 710 */ 711 private void logWrongImportGroupOrder(DetailAST importAST, String importGroup, 712 String currentGroupNumber, String fullImportIdent) { 713 if (NON_GROUP_RULE_GROUP.equals(importGroup)) { 714 log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent); 715 } 716 else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) { 717 log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent); 718 } 719 else { 720 log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent); 721 } 722 } 723 724 /** 725 * Get next import group. 726 * 727 * @param currentGroupNumber 728 * current group number. 729 * @return 730 * next import group. 731 */ 732 private String getNextImportGroup(int currentGroupNumber) { 733 int nextGroupNumber = currentGroupNumber; 734 735 while (customOrderRules.size() > nextGroupNumber + 1) { 736 if (hasAnyImportInCurrentGroup(customOrderRules.get(nextGroupNumber))) { 737 break; 738 } 739 nextGroupNumber++; 740 } 741 return customOrderRules.get(nextGroupNumber); 742 } 743 744 /** 745 * Checks if current group contains any import. 746 * 747 * @param currentGroup 748 * current group. 749 * @return 750 * true, if current group contains at least one import. 751 */ 752 private boolean hasAnyImportInCurrentGroup(String currentGroup) { 753 boolean result = false; 754 for (ImportDetails currentImport : importToGroupList) { 755 if (currentGroup.equals(currentImport.getImportGroup())) { 756 result = true; 757 break; 758 } 759 } 760 return result; 761 } 762 763 /** 764 * Get import valid group. 765 * 766 * @param isStatic 767 * is static import. 768 * @param importPath 769 * full import path. 770 * @return import valid group. 771 */ 772 private String getImportGroup(boolean isStatic, String importPath) { 773 RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0); 774 if (isStatic && customOrderRules.contains(STATIC_RULE_GROUP)) { 775 bestMatch.group = STATIC_RULE_GROUP; 776 bestMatch.matchLength = importPath.length(); 777 } 778 else if (customOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) { 779 final String importPathTrimmedToSamePackageDepth = 780 getFirstDomainsFromIdent(samePackageMatchingDepth, importPath); 781 if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) { 782 bestMatch.group = SAME_PACKAGE_RULE_GROUP; 783 bestMatch.matchLength = importPath.length(); 784 } 785 } 786 for (String group : customOrderRules) { 787 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) { 788 bestMatch = findBetterPatternMatch(importPath, 789 STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch); 790 } 791 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) { 792 bestMatch = findBetterPatternMatch(importPath, 793 group, specialImportsRegExp, bestMatch); 794 } 795 } 796 797 if (bestMatch.group.equals(NON_GROUP_RULE_GROUP) 798 && customOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP) 799 && thirdPartyPackageRegExp.matcher(importPath).find()) { 800 bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP; 801 } 802 return bestMatch.group; 803 } 804 805 /** 806 * Tries to find better matching regular expression: 807 * longer matching substring wins; in case of the same length, 808 * lower position of matching substring wins. 809 * 810 * @param importPath 811 * Full import identifier 812 * @param group 813 * Import group we are trying to assign the import 814 * @param regExp 815 * Regular expression for import group 816 * @param currentBestMatch 817 * object with currently best match 818 * @return better match (if found) or the same (currentBestMatch) 819 */ 820 private static RuleMatchForImport findBetterPatternMatch(String importPath, String group, 821 Pattern regExp, RuleMatchForImport currentBestMatch) { 822 RuleMatchForImport betterMatchCandidate = currentBestMatch; 823 final Matcher matcher = regExp.matcher(importPath); 824 while (matcher.find()) { 825 final int length = matcher.end() - matcher.start(); 826 if (length > betterMatchCandidate.matchLength 827 || length == betterMatchCandidate.matchLength 828 && matcher.start() < betterMatchCandidate.matchPosition) { 829 betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start()); 830 } 831 } 832 return betterMatchCandidate; 833 } 834 835 /** 836 * Checks compare two import paths. 837 * 838 * @param import1 839 * current import. 840 * @param import2 841 * previous import. 842 * @return a negative integer, zero, or a positive integer as the 843 * specified String is greater than, equal to, or less 844 * than this String, ignoring case considerations. 845 */ 846 private static int compareImports(String import1, String import2) { 847 int result = 0; 848 final String separator = "\\."; 849 final String[] import1Tokens = import1.split(separator); 850 final String[] import2Tokens = import2.split(separator); 851 for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) { 852 final String import1Token = import1Tokens[i]; 853 final String import2Token = import2Tokens[i]; 854 result = import1Token.compareTo(import2Token); 855 if (result != 0) { 856 break; 857 } 858 } 859 if (result == 0) { 860 result = Integer.compare(import1Tokens.length, import2Tokens.length); 861 } 862 return result; 863 } 864 865 /** 866 * Counts empty lines between given parameters. 867 * 868 * @param fromLineNo 869 * One-based line number of previous import. 870 * @param toLineNo 871 * One-based line number of current import. 872 * @return count of empty lines between given parameters, exclusive, 873 * eg., (fromLineNo, toLineNo). 874 */ 875 private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) { 876 int result = 0; 877 final String[] lines = getLines(); 878 879 for (int i = fromLineNo + 1; i <= toLineNo - 1; i++) { 880 // "- 1" because the numbering is one-based 881 if (CommonUtil.isBlank(lines[i - 1])) { 882 result++; 883 } 884 } 885 return result; 886 } 887 888 /** 889 * Forms import full path. 890 * 891 * @param token 892 * current token. 893 * @return full path or null. 894 */ 895 private static String getFullImportIdent(DetailAST token) { 896 String ident = ""; 897 if (token != null) { 898 ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText(); 899 } 900 return ident; 901 } 902 903 /** 904 * Parses ordering rule and adds it to the list with rules. 905 * 906 * @param ruleStr 907 * String with rule. 908 * @throws IllegalArgumentException when SAME_PACKAGE rule parameter is not positive integer 909 * @throws IllegalStateException when ruleStr is unexpected value 910 */ 911 private void addRulesToList(String ruleStr) { 912 if (STATIC_RULE_GROUP.equals(ruleStr) 913 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr) 914 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr) 915 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) { 916 customOrderRules.add(ruleStr); 917 } 918 else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) { 919 final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1, 920 ruleStr.indexOf(')')); 921 samePackageMatchingDepth = Integer.parseInt(rule); 922 if (samePackageMatchingDepth <= 0) { 923 throw new IllegalArgumentException( 924 "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr); 925 } 926 customOrderRules.add(SAME_PACKAGE_RULE_GROUP); 927 } 928 else { 929 throw new IllegalStateException("Unexpected rule: " + ruleStr); 930 } 931 } 932 933 /** 934 * Creates samePackageDomainsRegExp of the first package domains. 935 * 936 * @param firstPackageDomainsCount 937 * number of first package domains. 938 * @param packageNode 939 * package node. 940 * @return same package regexp. 941 */ 942 private static String createSamePackageRegexp(int firstPackageDomainsCount, 943 DetailAST packageNode) { 944 final String packageFullPath = getFullImportIdent(packageNode); 945 return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath); 946 } 947 948 /** 949 * Extracts defined amount of domains from the left side of package/import identifier. 950 * 951 * @param firstPackageDomainsCount 952 * number of first package domains. 953 * @param packageFullPath 954 * full identifier containing path to package or imported object. 955 * @return String with defined amount of domains or full identifier 956 * (if full identifier had less domain than specified) 957 */ 958 private static String getFirstDomainsFromIdent( 959 final int firstPackageDomainsCount, final String packageFullPath) { 960 final StringBuilder builder = new StringBuilder(256); 961 final StringTokenizer tokens = new StringTokenizer(packageFullPath, "."); 962 int count = firstPackageDomainsCount; 963 964 while (count > 0 && tokens.hasMoreTokens()) { 965 builder.append(tokens.nextToken()).append('.'); 966 count--; 967 } 968 return builder.toString(); 969 } 970 971 /** 972 * Contains import attributes as line number, import full path, import 973 * group. 974 */ 975 private static class ImportDetails { 976 977 /** Import full path. */ 978 private final String importFullPath; 979 980 /** Import group. */ 981 private final String importGroup; 982 983 /** Is static import. */ 984 private final boolean staticImport; 985 986 /** Import AST. */ 987 private final DetailAST importAST; 988 989 /** 990 * Initialise importFullPath, importGroup, staticImport, importAST. 991 * 992 * @param importFullPath 993 * import full path. 994 * @param importGroup 995 * import group. 996 * @param staticImport 997 * if import is static. 998 * @param importAST 999 * import ast 1000 */ 1001 /* package */ ImportDetails(String importFullPath, String importGroup, boolean staticImport, 1002 DetailAST importAST) { 1003 this.importFullPath = importFullPath; 1004 this.importGroup = importGroup; 1005 this.staticImport = staticImport; 1006 this.importAST = importAST; 1007 } 1008 1009 /** 1010 * Get import full path variable. 1011 * 1012 * @return import full path variable. 1013 */ 1014 public String getImportFullPath() { 1015 return importFullPath; 1016 } 1017 1018 /** 1019 * Get import start line number from ast. 1020 * 1021 * @return import start line from ast. 1022 */ 1023 public int getStartLineNumber() { 1024 return importAST.getLineNo(); 1025 } 1026 1027 /** 1028 * Get import end line number from ast. 1029 * <p> 1030 * <b>Note:</b> It can be different from <b>startLineNumber</b> when import statement span 1031 * multiple lines. 1032 * </p> 1033 * @return import end line from ast. 1034 */ 1035 public int getEndLineNumber() { 1036 return importAST.getLastChild().getLineNo(); 1037 } 1038 1039 /** 1040 * Get import group. 1041 * 1042 * @return import group. 1043 */ 1044 public String getImportGroup() { 1045 return importGroup; 1046 } 1047 1048 /** 1049 * Checks if import is static. 1050 * 1051 * @return true, if import is static. 1052 */ 1053 public boolean isStaticImport() { 1054 return staticImport; 1055 } 1056 1057 /** 1058 * Get import ast. 1059 * 1060 * @return import ast. 1061 */ 1062 public DetailAST getImportAST() { 1063 return importAST; 1064 } 1065 1066 } 1067 1068 /** 1069 * Contains matching attributes assisting in definition of "best matching" 1070 * group for import. 1071 */ 1072 private static class RuleMatchForImport { 1073 1074 /** Position of matching string for current best match. */ 1075 private final int matchPosition; 1076 /** Length of matching string for current best match. */ 1077 private int matchLength; 1078 /** Import group for current best match. */ 1079 private String group; 1080 1081 /** 1082 * Constructor to initialize the fields. 1083 * 1084 * @param group 1085 * Matched group. 1086 * @param length 1087 * Matching length. 1088 * @param position 1089 * Matching position. 1090 */ 1091 /* package */ RuleMatchForImport(String group, int length, int position) { 1092 this.group = group; 1093 matchLength = length; 1094 matchPosition = position; 1095 } 1096 1097 } 1098 1099}