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