001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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.Locale; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.FullIdent; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 031 032/** 033 * <ul> 034 * <li>groups imports: ensures that groups of imports come in a specific order 035 * (e.g., java. comes first, javax. comes second, then everything else)</li> 036 * <li>adds a separation between groups : ensures that a blank line sit between 037 * each group</li> 038 * <li>import groups aren't separated internally: ensures that 039 * each group aren't separated internally by blank line or comment</li> 040 * <li>sorts imports inside each group: ensures that imports within each group 041 * are in lexicographic order</li> 042 * <li>sorts according to case: ensures that the comparison between import is 043 * case sensitive</li> 044 * <li>groups static imports: ensures that static imports are at the top (or the 045 * bottom) of all the imports, or above (or under) each group, or are treated 046 * like non static imports (@see {@link ImportOrderOption}</li> 047 * </ul>. 048 * 049 * <pre> 050 * Properties: 051 * </pre> 052 * <table summary="Properties" border="1"> 053 * <tr><th>name</th><th>Description</th><th>type</th><th>default value</th></tr> 054 * <tr><td>option</td><td>policy on the relative order between regular imports and static 055 * imports</td><td>{@link ImportOrderOption}</td><td>under</td></tr> 056 * <tr><td>groups</td><td>list of imports groups (every group identified either by a common 057 * prefix string, or by a regular expression enclosed in forward slashes (e.g. /regexp/)</td> 058 * <td>list of strings</td><td>empty list</td></tr> 059 * <tr><td>ordered</td><td>whether imports within group should be sorted</td> 060 * <td>Boolean</td><td>true</td></tr> 061 * <tr><td>separated</td><td>whether imports groups should be separated by, at least, 062 * one blank line or comment and aren't separated internally 063 * </td><td>Boolean</td><td>false</td></tr> 064 * <tr><td>caseSensitive</td><td>whether string comparison should be case sensitive or not. 065 * Case sensitive sorting is in ASCII sort order</td><td>Boolean</td><td>true</td></tr> 066 * <tr><td>sortStaticImportsAlphabetically</td><td>whether static imports grouped by top or 067 * bottom option are sorted alphabetically or not</td><td>Boolean</td><td>false</td></tr> 068 * <tr><td>useContainerOrderingForStatic</td><td>whether to use container ordering 069 * (Eclipse IDE term) for static imports or not</td><td>Boolean</td><td>false</td></tr> 070 * </table> 071 * 072 * <p> 073 * Example: 074 * </p> 075 * <p>To configure the check so that it matches default Eclipse formatter configuration 076 * (tested on Kepler, Luna and Mars):</p> 077 * <ul> 078 * <li>group of static imports is on the top</li> 079 * <li>groups of non-static imports: "java" then "javax" 080 * packages first, then "org" and then all other imports</li> 081 * <li>imports will be sorted in the groups</li> 082 * <li>groups are separated by, at least, one blank line and aren't separated internally</li> 083 * </ul> 084 * 085 * <pre> 086 * <module name="ImportOrder"> 087 * <property name="groups" value="/^javax?\./,org"/> 088 * <property name="ordered" value="true"/> 089 * <property name="separated" value="true"/> 090 * <property name="option" value="above"/> 091 * <property name="sortStaticImportsAlphabetically" value="true"/> 092 * </module> 093 * </pre> 094 * 095 * <p>To configure the check so that it matches default IntelliJ IDEA formatter configuration 096 * (tested on v14):</p> 097 * <ul> 098 * <li>group of static imports is on the bottom</li> 099 * <li>groups of non-static imports: all imports except of "javax" and 100 * "java", then "javax" and "java"</li> 101 * <li>imports will be sorted in the groups</li> 102 * <li>groups are separated by, at least, one blank line and aren't separated internally</li> 103 * </ul> 104 * 105 * <p> 106 * Note: "separated" option is disabled because IDEA default has blank line 107 * between "java" and static imports, and no blank line between 108 * "javax" and "java" 109 * </p> 110 * 111 * <pre> 112 * <module name="ImportOrder"> 113 * <property name="groups" value="*,javax,java"/> 114 * <property name="ordered" value="true"/> 115 * <property name="separated" value="false"/> 116 * <property name="option" value="bottom"/> 117 * <property name="sortStaticImportsAlphabetically" value="true"/> 118 * </module> 119 * </pre> 120 * 121 * <p>To configure the check so that it matches default NetBeans formatter configuration 122 * (tested on v8):</p> 123 * <ul> 124 * <li>groups of non-static imports are not defined, all imports will be sorted 125 * as a one group</li> 126 * <li>static imports are not separated, they will be sorted along with other imports</li> 127 * </ul> 128 * 129 * <pre> 130 * <module name="ImportOrder"> 131 * <property name="option" value="inflow"/> 132 * </module> 133 * </pre> 134 * 135 * <p> 136 * Group descriptions enclosed in slashes are interpreted as regular 137 * expressions. If multiple groups match, the one matching a longer 138 * substring of the imported name will take precedence, with ties 139 * broken first in favor of earlier matches and finally in favor of 140 * the first matching group. 141 * </p> 142 * 143 * <p> 144 * There is always a wildcard group to which everything not in a named group 145 * belongs. If an import does not match a named group, the group belongs to 146 * this wildcard group. The wildcard group position can be specified using the 147 * {@code *} character. 148 * </p> 149 * 150 * <p>Check also has on option making it more flexible: 151 * <b>sortStaticImportsAlphabetically</b> - sets whether static imports grouped by 152 * <b>top</b> or <b>bottom</b> option should be sorted alphabetically or 153 * not, default value is <b>false</b>. It is applied to static imports grouped 154 * with <b>top</b> or <b>bottom</b> options.<br> 155 * This option is helping in reconciling of this Check and other tools like 156 * Eclipse's Organize Imports feature. 157 * </p> 158 * <p> 159 * To configure the Check allows static imports grouped to the <b>top</b> 160 * being sorted alphabetically: 161 * </p> 162 * 163 * <pre> 164 * {@code 165 * import static java.lang.Math.abs; 166 * import static org.abego.treelayout.Configuration.AlignmentInLevel; // OK, alphabetical order 167 * 168 * import org.abego.*; 169 * 170 * import java.util.Set; 171 * 172 * public class SomeClass { ... } 173 * } 174 * </pre> 175 * 176 * 177 * @author Bill Schneider 178 * @author o_sukhodolsky 179 * @author David DIDIER 180 * @author Steve McKay 181 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> 182 * @author Andrei Selkin 183 */ 184public class ImportOrderCheck 185 extends AbstractCheck { 186 187 /** 188 * A key is pointing to the warning message text in "messages.properties" 189 * file. 190 */ 191 public static final String MSG_SEPARATION = "import.separation"; 192 193 /** 194 * A key is pointing to the warning message text in "messages.properties" 195 * file. 196 */ 197 public static final String MSG_ORDERING = "import.ordering"; 198 199 /** 200 * A key is pointing to the warning message text in "messages.properties" 201 * file. 202 */ 203 public static final String MSG_SEPARATED_IN_GROUP = "import.groups.separated.internally"; 204 205 /** The special wildcard that catches all remaining groups. */ 206 private static final String WILDCARD_GROUP_NAME = "*"; 207 208 /** Empty array of pattern type needed to initialize check. */ 209 private static final Pattern[] EMPTY_PATTERN_ARRAY = new Pattern[0]; 210 211 /** List of import groups specified by the user. */ 212 private Pattern[] groups = EMPTY_PATTERN_ARRAY; 213 /** Require imports in group be separated. */ 214 private boolean separated; 215 /** Require imports in group. */ 216 private boolean ordered = true; 217 /** Should comparison be case sensitive. */ 218 private boolean caseSensitive = true; 219 220 /** Last imported group. */ 221 private int lastGroup; 222 /** Line number of last import. */ 223 private int lastImportLine; 224 /** Name of last import. */ 225 private String lastImport; 226 /** If last import was static. */ 227 private boolean lastImportStatic; 228 /** Whether there was any imports. */ 229 private boolean beforeFirstImport; 230 /** Whether static imports should be sorted alphabetically or not. */ 231 private boolean sortStaticImportsAlphabetically; 232 /** Whether to use container ordering (Eclipse IDE term) for static imports or not. */ 233 private boolean useContainerOrderingForStatic; 234 235 /** The policy to enforce. */ 236 private ImportOrderOption option = ImportOrderOption.UNDER; 237 238 /** 239 * Set the option to enforce. 240 * @param optionStr string to decode option from 241 * @throws IllegalArgumentException if unable to decode 242 */ 243 public void setOption(String optionStr) { 244 try { 245 option = ImportOrderOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH)); 246 } 247 catch (IllegalArgumentException iae) { 248 throw new IllegalArgumentException("unable to parse " + optionStr, iae); 249 } 250 } 251 252 /** 253 * Sets the list of package groups and the order they should occur in the 254 * file. 255 * 256 * @param packageGroups a comma-separated list of package names/prefixes. 257 */ 258 public void setGroups(String... packageGroups) { 259 groups = new Pattern[packageGroups.length]; 260 261 for (int i = 0; i < packageGroups.length; i++) { 262 String pkg = packageGroups[i]; 263 final Pattern grp; 264 265 // if the pkg name is the wildcard, make it match zero chars 266 // from any name, so it will always be used as last resort. 267 if (WILDCARD_GROUP_NAME.equals(pkg)) { 268 // matches any package 269 grp = Pattern.compile(""); 270 } 271 else if (CommonUtils.startsWithChar(pkg, '/')) { 272 if (!CommonUtils.endsWithChar(pkg, '/')) { 273 throw new IllegalArgumentException("Invalid group"); 274 } 275 pkg = pkg.substring(1, pkg.length() - 1); 276 grp = Pattern.compile(pkg); 277 } 278 else { 279 final StringBuilder pkgBuilder = new StringBuilder(pkg); 280 if (!CommonUtils.endsWithChar(pkg, '.')) { 281 pkgBuilder.append('.'); 282 } 283 grp = Pattern.compile("^" + Pattern.quote(pkgBuilder.toString())); 284 } 285 286 groups[i] = grp; 287 } 288 } 289 290 /** 291 * Sets whether or not imports should be ordered within any one group of 292 * imports. 293 * 294 * @param ordered 295 * whether lexicographic ordering of imports within a group 296 * required or not. 297 */ 298 public void setOrdered(boolean ordered) { 299 this.ordered = ordered; 300 } 301 302 /** 303 * Sets whether or not groups of imports must be separated from one another 304 * by at least one blank line. 305 * 306 * @param separated 307 * whether groups should be separated by oen blank line. 308 */ 309 public void setSeparated(boolean separated) { 310 this.separated = separated; 311 } 312 313 /** 314 * Sets whether string comparison should be case sensitive or not. 315 * 316 * @param caseSensitive 317 * whether string comparison should be case sensitive. 318 */ 319 public void setCaseSensitive(boolean caseSensitive) { 320 this.caseSensitive = caseSensitive; 321 } 322 323 /** 324 * Sets whether static imports (when grouped using 'top' and 'bottom' option) 325 * are sorted alphabetically or according to the package groupings. 326 * @param sortAlphabetically true or false. 327 */ 328 public void setSortStaticImportsAlphabetically(boolean sortAlphabetically) { 329 sortStaticImportsAlphabetically = sortAlphabetically; 330 } 331 332 /** 333 * Sets whether to use container ordering (Eclipse IDE term) for static imports or not. 334 * @param useContainerOrdering whether to use container ordering for static imports or not. 335 */ 336 public void setUseContainerOrderingForStatic(boolean useContainerOrdering) { 337 useContainerOrderingForStatic = useContainerOrdering; 338 } 339 340 @Override 341 public int[] getDefaultTokens() { 342 return getAcceptableTokens(); 343 } 344 345 @Override 346 public int[] getAcceptableTokens() { 347 return new int[] {TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT}; 348 } 349 350 @Override 351 public int[] getRequiredTokens() { 352 return new int[] {TokenTypes.IMPORT}; 353 } 354 355 @Override 356 public void beginTree(DetailAST rootAST) { 357 lastGroup = Integer.MIN_VALUE; 358 lastImportLine = Integer.MIN_VALUE; 359 lastImport = ""; 360 lastImportStatic = false; 361 beforeFirstImport = true; 362 } 363 364 // -@cs[CyclomaticComplexity] SWITCH was transformed into IF-ELSE. 365 @Override 366 public void visitToken(DetailAST ast) { 367 final FullIdent ident; 368 final boolean isStatic; 369 370 if (ast.getType() == TokenTypes.IMPORT) { 371 ident = FullIdent.createFullIdentBelow(ast); 372 isStatic = false; 373 } 374 else { 375 ident = FullIdent.createFullIdent(ast.getFirstChild() 376 .getNextSibling()); 377 isStatic = true; 378 } 379 380 final boolean isStaticAndNotLastImport = isStatic && !lastImportStatic; 381 final boolean isLastImportAndNonStatic = lastImportStatic && !isStatic; 382 383 // using set of IF instead of SWITCH to analyze Enum options to satisfy coverage. 384 // https://github.com/checkstyle/checkstyle/issues/1387 385 if (option == ImportOrderOption.TOP) { 386 387 if (isLastImportAndNonStatic) { 388 lastGroup = Integer.MIN_VALUE; 389 lastImport = ""; 390 } 391 doVisitToken(ident, isStatic, isStaticAndNotLastImport); 392 393 } 394 else if (option == ImportOrderOption.BOTTOM) { 395 396 if (isStaticAndNotLastImport) { 397 lastGroup = Integer.MIN_VALUE; 398 lastImport = ""; 399 } 400 doVisitToken(ident, isStatic, isLastImportAndNonStatic); 401 402 } 403 else if (option == ImportOrderOption.ABOVE) { 404 // previous non-static but current is static 405 doVisitToken(ident, isStatic, isStaticAndNotLastImport); 406 407 } 408 else if (option == ImportOrderOption.UNDER) { 409 doVisitToken(ident, isStatic, isLastImportAndNonStatic); 410 411 } 412 else if (option == ImportOrderOption.INFLOW) { 413 // "previous" argument is useless here 414 doVisitToken(ident, isStatic, true); 415 416 } 417 else { 418 throw new IllegalStateException( 419 "Unexpected option for static imports: " + option); 420 } 421 422 lastImportLine = ast.findFirstToken(TokenTypes.SEMI).getLineNo(); 423 lastImportStatic = isStatic; 424 beforeFirstImport = false; 425 } 426 427 /** 428 * Shares processing... 429 * 430 * @param ident the import to process. 431 * @param isStatic whether the token is static or not. 432 * @param previous previous non-static but current is static (above), or 433 * previous static but current is non-static (under). 434 */ 435 private void doVisitToken(FullIdent ident, boolean isStatic, 436 boolean previous) { 437 final String name = ident.getText(); 438 final int groupIdx = getGroupNumber(name); 439 final int line = ident.getLineNo(); 440 441 if (groupIdx == lastGroup 442 || !beforeFirstImport && isAlphabeticallySortableStaticImport(isStatic)) { 443 doVisitTokenInSameGroup(isStatic, previous, name, line); 444 } 445 else if (groupIdx > lastGroup) { 446 if (!beforeFirstImport && separated && line - lastImportLine < 2) { 447 log(line, MSG_SEPARATION, name); 448 } 449 } 450 else { 451 log(line, MSG_ORDERING, name); 452 } 453 if (isSeparatorInGroup(groupIdx, isStatic, line)) { 454 log(line, MSG_SEPARATED_IN_GROUP, name); 455 } 456 457 lastGroup = groupIdx; 458 lastImport = name; 459 } 460 461 /** 462 * Checks whether imports group separated internally. 463 * @param groupIdx group number. 464 * @param isStatic whether the token is static or not. 465 * @param line the line of the current import. 466 * @return true if imports group are separated internally. 467 */ 468 private boolean isSeparatorInGroup(int groupIdx, boolean isStatic, int line) { 469 final boolean inSameGroup = isInSameGroup(groupIdx, isStatic); 470 return (!separated || inSameGroup) && isSeparatorBeforeImport(line); 471 } 472 473 /** 474 * Checks whether there is any separator before current import. 475 * @param line the line of the current import. 476 * @return true if there is separator before current import which isn't the first import. 477 */ 478 private boolean isSeparatorBeforeImport(int line) { 479 return !beforeFirstImport && line - lastImportLine > 1; 480 } 481 482 /** 483 * Checks whether imports are in same group. 484 * @param groupIdx group number. 485 * @param isStatic whether the token is static or not. 486 * @return true if imports are in same group. 487 */ 488 private boolean isInSameGroup(int groupIdx, boolean isStatic) { 489 final boolean isStaticImportGroupIndependent = 490 option == ImportOrderOption.TOP || option == ImportOrderOption.BOTTOM; 491 final boolean result; 492 if (isStaticImportGroupIndependent) { 493 result = isStatic && lastImportStatic 494 || groupIdx == lastGroup && isStatic == lastImportStatic; 495 } 496 else { 497 result = groupIdx == lastGroup; 498 } 499 return result; 500 } 501 502 /** 503 * Checks whether static imports grouped by <b>top</b> or <b>bottom</b> option 504 * are sorted alphabetically or not. 505 * @param isStatic if current import is static. 506 * @return true if static imports should be sorted alphabetically. 507 */ 508 private boolean isAlphabeticallySortableStaticImport(boolean isStatic) { 509 return isStatic && sortStaticImportsAlphabetically 510 && (option == ImportOrderOption.TOP 511 || option == ImportOrderOption.BOTTOM); 512 } 513 514 /** 515 * Shares processing... 516 * 517 * @param isStatic whether the token is static or not. 518 * @param previous previous non-static but current is static (above), or 519 * previous static but current is non-static (under). 520 * @param name the name of the current import. 521 * @param line the line of the current import. 522 */ 523 private void doVisitTokenInSameGroup(boolean isStatic, 524 boolean previous, String name, int line) { 525 if (ordered) { 526 if (option == ImportOrderOption.INFLOW) { 527 if (isWrongOrder(name, isStatic)) { 528 log(line, MSG_ORDERING, name); 529 } 530 } 531 else { 532 final boolean shouldFireError = 533 // previous non-static but current is static (above) 534 // or 535 // previous static but current is non-static (under) 536 previous 537 || 538 // current and previous static or current and 539 // previous non-static 540 lastImportStatic == isStatic 541 && isWrongOrder(name, isStatic); 542 543 if (shouldFireError) { 544 log(line, MSG_ORDERING, name); 545 } 546 } 547 } 548 } 549 550 /** 551 * Checks whether import name is in wrong order. 552 * @param name import name. 553 * @param isStatic whether it is a static import name. 554 * @return true if import name is in wrong order. 555 */ 556 private boolean isWrongOrder(String name, boolean isStatic) { 557 final boolean result; 558 if (isStatic && useContainerOrderingForStatic) { 559 result = compareContainerOrder(lastImport, name, caseSensitive) > 0; 560 } 561 else { 562 // out of lexicographic order 563 result = compare(lastImport, name, caseSensitive) > 0; 564 } 565 return result; 566 } 567 568 /** 569 * Compares two import strings. 570 * We first compare the container of the static import, container being the type enclosing 571 * the static element being imported. When this returns 0, we compare the qualified 572 * import name. For e.g. this is what is considered to be container names: 573 * <p> 574 * import static HttpConstants.COLON => HttpConstants 575 * import static HttpHeaders.addHeader => HttpHeaders 576 * import static HttpHeaders.setHeader => HttpHeaders 577 * import static HttpHeaders.Names.DATE => HttpHeaders.Names 578 * </p> 579 * <p> 580 * According to this logic, HttpHeaders.Names would come after HttpHeaders. 581 * 582 * For more details, see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=473629#c3"> 583 * static imports comparison method</a> in Eclipse. 584 * </p> 585 * 586 * @param importName1 first import name. 587 * @param importName2 second import name. 588 * @param caseSensitive whether the comparison of fully qualified import names is case 589 * sensitive. 590 * @return the value {@code 0} if str1 is equal to str2; a value 591 * less than {@code 0} if str is less than the str2 (container order 592 * or lexicographical); and a value greater than {@code 0} if str1 is greater than str2 593 * (container order or lexicographically). 594 */ 595 private static int compareContainerOrder(String importName1, String importName2, 596 boolean caseSensitive) { 597 final String container1 = getImportContainer(importName1); 598 final String container2 = getImportContainer(importName2); 599 final int compareContainersOrderResult; 600 if (caseSensitive) { 601 compareContainersOrderResult = container1.compareTo(container2); 602 } 603 else { 604 compareContainersOrderResult = container1.compareToIgnoreCase(container2); 605 } 606 final int result; 607 if (compareContainersOrderResult == 0) { 608 result = compare(importName1, importName2, caseSensitive); 609 } 610 else { 611 result = compareContainersOrderResult; 612 } 613 return result; 614 } 615 616 /** 617 * Extracts import container name from fully qualified import name. 618 * An import container name is the type which encloses the static element being imported. 619 * For example, HttpConstants, HttpHeaders, HttpHeaders.Names are import container names: 620 * <p> 621 * import static HttpConstants.COLON => HttpConstants 622 * import static HttpHeaders.addHeader => HttpHeaders 623 * import static HttpHeaders.setHeader => HttpHeaders 624 * import static HttpHeaders.Names.DATE => HttpHeaders.Names 625 * </p> 626 * @param qualifiedImportName fully qualified import name. 627 * @return import container name. 628 */ 629 private static String getImportContainer(String qualifiedImportName) { 630 final int lastDotIndex = qualifiedImportName.lastIndexOf('.'); 631 return qualifiedImportName.substring(0, lastDotIndex); 632 } 633 634 /** 635 * Finds out what group the specified import belongs to. 636 * 637 * @param name the import name to find. 638 * @return group number for given import name. 639 */ 640 private int getGroupNumber(String name) { 641 int bestIndex = groups.length; 642 int bestLength = -1; 643 int bestPos = 0; 644 645 // find out what group this belongs in 646 // loop over groups and get index 647 for (int i = 0; i < groups.length; i++) { 648 final Matcher matcher = groups[i].matcher(name); 649 while (matcher.find()) { 650 final int length = matcher.end() - matcher.start(); 651 if (length > bestLength 652 || length == bestLength && matcher.start() < bestPos) { 653 bestIndex = i; 654 bestLength = length; 655 bestPos = matcher.start(); 656 } 657 } 658 } 659 660 return bestIndex; 661 } 662 663 /** 664 * Compares two strings. 665 * 666 * @param string1 667 * the first string. 668 * @param string2 669 * the second string. 670 * @param caseSensitive 671 * whether the comparison is case sensitive. 672 * @return the value {@code 0} if string1 is equal to string2; a value 673 * less than {@code 0} if string1 is lexicographically less 674 * than the string2; and a value greater than {@code 0} if 675 * string1 is lexicographically greater than string2. 676 */ 677 private static int compare(String string1, String string2, 678 boolean caseSensitive) { 679 final int result; 680 if (caseSensitive) { 681 result = string1.compareTo(string2); 682 } 683 else { 684 result = string1.compareToIgnoreCase(string2); 685 } 686 687 return result; 688 } 689}