001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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 org.apache.commons.beanutils.ConversionException; 027 028import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.FullIdent; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 033 034/** 035 * <ul> 036 * <li>groups imports: ensures that groups of imports come in a specific order 037 * (e.g., java. comes first, javax. comes second, then everything else)</li> 038 * <li>adds a separation between groups : ensures that a blank line sit between 039 * each group</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</td><td>Boolean</td><td>false</td></tr> 063 * <tr><td>caseSensitive</td><td>whether string comparison should be case sensitive or not. 064 * Case sensitive sorting is in ASCII sort order</td><td>Boolean</td><td>true</td></tr> 065 * <tr><td>sortStaticImportsAlphabetically</td><td>whether static imports grouped by top or 066 * bottom option are sorted alphabetically or not</td><td>Boolean</td><td>false</td></tr> 067 * </table> 068 * 069 * <p> 070 * Example: 071 * </p> 072 * <p>To configure the check so that it matches default Eclipse formatter configuration 073 * (tested on Kepler, Luna and Mars):</p> 074 * <ul> 075 * <li>group of static imports is on the top</li> 076 * <li>groups of non-static imports: "java" then "javax" 077 * packages first, then "org" and then all other imports</li> 078 * <li>imports will be sorted in the groups</li> 079 * <li>groups are separated by, at least, one blank line</li> 080 * </ul> 081 * 082 * <pre> 083 * <module name="ImportOrder"> 084 * <property name="groups" value="/^javax?\./,org"/> 085 * <property name="ordered" value="true"/> 086 * <property name="separated" value="true"/> 087 * <property name="option" value="above"/> 088 * <property name="sortStaticImportsAlphabetically" value="true"/> 089 * </module> 090 * </pre> 091 * 092 * <p>To configure the check so that it matches default IntelliJ IDEA formatter configuration 093 * (tested on v14):</p> 094 * <ul> 095 * <li>group of static imports is on the bottom</li> 096 * <li>groups of non-static imports: all imports except of "javax" and 097 * "java", then "javax" and "java"</li> 098 * <li>imports will be sorted in the groups</li> 099 * <li>groups are separated by, at least, one blank line</li> 100 * </ul> 101 * 102 * <p> 103 * Note: "separated" option is disabled because IDEA default has blank line 104 * between "java" and static imports, and no blank line between 105 * "javax" and "java" 106 * </p> 107 * 108 * <pre> 109 * <module name="ImportOrder"> 110 * <property name="groups" value="*,javax,java"/> 111 * <property name="ordered" value="true"/> 112 * <property name="separated" value="false"/> 113 * <property name="option" value="bottom"/> 114 * <property name="sortStaticImportsAlphabetically" value="true"/> 115 * </module> 116 * </pre> 117 * 118 * <p>To configure the check so that it matches default NetBeans formatter configuration 119 * (tested on v8):</p> 120 * <ul> 121 * <li>groups of non-static imports are not defined, all imports will be sorted 122 * as a one group</li> 123 * <li>static imports are not separated, they will be sorted along with other imports</li> 124 * </ul> 125 * 126 * <pre> 127 * <module name="ImportOrder"> 128 * <property name="option" value="inflow"/> 129 * </module> 130 * </pre> 131 * 132 * <p> 133 * Group descriptions enclosed in slashes are interpreted as regular 134 * expressions. If multiple groups match, the one matching a longer 135 * substring of the imported name will take precedence, with ties 136 * broken first in favor of earlier matches and finally in favor of 137 * the first matching group. 138 * </p> 139 * 140 * <p> 141 * There is always a wildcard group to which everything not in a named group 142 * belongs. If an import does not match a named group, the group belongs to 143 * this wildcard group. The wildcard group position can be specified using the 144 * {@code *} character. 145 * </p> 146 * 147 * <p>Check also has on option making it more flexible: 148 * <b>sortStaticImportsAlphabetically</b> - sets whether static imports grouped by 149 * <b>top</b> or <b>bottom</b> option should be sorted alphabetically or 150 * not, default value is <b>false</b>. It is applied to static imports grouped 151 * with <b>top</b> or <b>bottom</b> options.<br> 152 * This option is helping in reconciling of this Check and other tools like 153 * Eclipse's Organize Imports feature. 154 * </p> 155 * <p> 156 * To configure the Check allows static imports grouped to the <b>top</b> 157 * being sorted alphabetically: 158 * </p> 159 * 160 * <pre> 161 * {@code 162 * import static java.lang.Math.abs; 163 * import static org.abego.treelayout.Configuration.AlignmentInLevel; // OK, alphabetical order 164 * 165 * import org.abego.*; 166 * 167 * import java.util.Set; 168 * 169 * public class SomeClass { ... } 170 * } 171 * </pre> 172 * 173 * 174 * @author Bill Schneider 175 * @author o_sukhodolsky 176 * @author David DIDIER 177 * @author Steve McKay 178 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> 179 */ 180public class ImportOrderCheck 181 extends AbstractCheck { 182 183 /** 184 * A key is pointing to the warning message text in "messages.properties" 185 * file. 186 */ 187 public static final String MSG_SEPARATION = "import.separation"; 188 189 /** 190 * A key is pointing to the warning message text in "messages.properties" 191 * file. 192 */ 193 public static final String MSG_ORDERING = "import.ordering"; 194 195 /** The special wildcard that catches all remaining groups. */ 196 private static final String WILDCARD_GROUP_NAME = "*"; 197 198 /** Empty array of pattern type needed to initialize check. */ 199 private static final Pattern[] EMPTY_PATTERN_ARRAY = new Pattern[0]; 200 201 /** List of import groups specified by the user. */ 202 private Pattern[] groups = EMPTY_PATTERN_ARRAY; 203 /** Require imports in group be separated. */ 204 private boolean separated; 205 /** Require imports in group. */ 206 private boolean ordered = true; 207 /** Should comparison be case sensitive. */ 208 private boolean caseSensitive = true; 209 210 /** Last imported group. */ 211 private int lastGroup; 212 /** Line number of last import. */ 213 private int lastImportLine; 214 /** Name of last import. */ 215 private String lastImport; 216 /** If last import was static. */ 217 private boolean lastImportStatic; 218 /** Whether there was any imports. */ 219 private boolean beforeFirstImport; 220 /** Whether static imports should be sorted alphabetically or not. */ 221 private boolean sortStaticImportsAlphabetically; 222 223 /** The policy to enforce. */ 224 private ImportOrderOption option = ImportOrderOption.UNDER; 225 226 /** 227 * Set the option to enforce. 228 * @param optionStr string to decode option from 229 * @throws ConversionException if unable to decode 230 */ 231 public void setOption(String optionStr) { 232 try { 233 option = ImportOrderOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH)); 234 } 235 catch (IllegalArgumentException iae) { 236 throw new ConversionException("unable to parse " + optionStr, iae); 237 } 238 } 239 240 /** 241 * Sets the list of package groups and the order they should occur in the 242 * file. 243 * 244 * @param packageGroups a comma-separated list of package names/prefixes. 245 */ 246 public void setGroups(String... packageGroups) { 247 groups = new Pattern[packageGroups.length]; 248 249 for (int i = 0; i < packageGroups.length; i++) { 250 String pkg = packageGroups[i]; 251 final StringBuilder pkgBuilder = new StringBuilder(pkg); 252 final Pattern grp; 253 254 // if the pkg name is the wildcard, make it match zero chars 255 // from any name, so it will always be used as last resort. 256 if (WILDCARD_GROUP_NAME.equals(pkg)) { 257 // matches any package 258 grp = Pattern.compile(""); 259 } 260 else if (CommonUtils.startsWithChar(pkg, '/')) { 261 if (!CommonUtils.endsWithChar(pkg, '/')) { 262 throw new IllegalArgumentException("Invalid group"); 263 } 264 pkg = pkg.substring(1, pkg.length() - 1); 265 grp = Pattern.compile(pkg); 266 } 267 else { 268 if (!CommonUtils.endsWithChar(pkg, '.')) { 269 pkgBuilder.append('.'); 270 } 271 grp = Pattern.compile("^" + Pattern.quote(pkgBuilder.toString())); 272 } 273 274 groups[i] = grp; 275 } 276 } 277 278 /** 279 * Sets whether or not imports should be ordered within any one group of 280 * imports. 281 * 282 * @param ordered 283 * whether lexicographic ordering of imports within a group 284 * required or not. 285 */ 286 public void setOrdered(boolean ordered) { 287 this.ordered = ordered; 288 } 289 290 /** 291 * Sets whether or not groups of imports must be separated from one another 292 * by at least one blank line. 293 * 294 * @param separated 295 * whether groups should be separated by oen blank line. 296 */ 297 public void setSeparated(boolean separated) { 298 this.separated = separated; 299 } 300 301 /** 302 * Sets whether string comparison should be case sensitive or not. 303 * 304 * @param caseSensitive 305 * whether string comparison should be case sensitive. 306 */ 307 public void setCaseSensitive(boolean caseSensitive) { 308 this.caseSensitive = caseSensitive; 309 } 310 311 /** 312 * Sets whether static imports (when grouped using 'top' and 'bottom' option) 313 * are sorted alphabetically or according to the package groupings. 314 * @param sortAlphabetically true or false. 315 */ 316 public void setSortStaticImportsAlphabetically(boolean sortAlphabetically) { 317 sortStaticImportsAlphabetically = sortAlphabetically; 318 } 319 320 @Override 321 public int[] getDefaultTokens() { 322 return getAcceptableTokens(); 323 } 324 325 @Override 326 public int[] getAcceptableTokens() { 327 return new int[] {TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT}; 328 } 329 330 @Override 331 public int[] getRequiredTokens() { 332 return new int[] {TokenTypes.IMPORT}; 333 } 334 335 @Override 336 public void beginTree(DetailAST rootAST) { 337 lastGroup = Integer.MIN_VALUE; 338 lastImportLine = Integer.MIN_VALUE; 339 lastImport = ""; 340 lastImportStatic = false; 341 beforeFirstImport = true; 342 } 343 344 @Override 345 public void visitToken(DetailAST ast) { 346 final FullIdent ident; 347 final boolean isStatic; 348 349 if (ast.getType() == TokenTypes.IMPORT) { 350 ident = FullIdent.createFullIdentBelow(ast); 351 isStatic = false; 352 } 353 else { 354 ident = FullIdent.createFullIdent(ast.getFirstChild() 355 .getNextSibling()); 356 isStatic = true; 357 } 358 359 final boolean isStaticAndNotLastImport = isStatic && !lastImportStatic; 360 final boolean isLastImportAndNonStatic = lastImportStatic && !isStatic; 361 362 // using set of IF instead of SWITCH to analyze Enum options to satisfy coverage. 363 // https://github.com/checkstyle/checkstyle/issues/1387 364 if (option == ImportOrderOption.TOP) { 365 366 if (isLastImportAndNonStatic) { 367 lastGroup = Integer.MIN_VALUE; 368 lastImport = ""; 369 } 370 doVisitToken(ident, isStatic, isStaticAndNotLastImport); 371 372 } 373 else if (option == ImportOrderOption.BOTTOM) { 374 375 if (isStaticAndNotLastImport) { 376 lastGroup = Integer.MIN_VALUE; 377 lastImport = ""; 378 } 379 doVisitToken(ident, isStatic, isLastImportAndNonStatic); 380 381 } 382 else if (option == ImportOrderOption.ABOVE) { 383 // previous non-static but current is static 384 doVisitToken(ident, isStatic, isStaticAndNotLastImport); 385 386 } 387 else if (option == ImportOrderOption.UNDER) { 388 doVisitToken(ident, isStatic, isLastImportAndNonStatic); 389 390 } 391 else if (option == ImportOrderOption.INFLOW) { 392 // "previous" argument is useless here 393 doVisitToken(ident, isStatic, true); 394 395 } 396 else { 397 throw new IllegalStateException( 398 "Unexpected option for static imports: " + option); 399 } 400 401 lastImportLine = ast.findFirstToken(TokenTypes.SEMI).getLineNo(); 402 lastImportStatic = isStatic; 403 beforeFirstImport = false; 404 } 405 406 /** 407 * Shares processing... 408 * 409 * @param ident the import to process. 410 * @param isStatic whether the token is static or not. 411 * @param previous previous non-static but current is static (above), or 412 * previous static but current is non-static (under). 413 */ 414 private void doVisitToken(FullIdent ident, boolean isStatic, 415 boolean previous) { 416 final String name = ident.getText(); 417 final int groupIdx = getGroupNumber(name); 418 final int line = ident.getLineNo(); 419 420 if (groupIdx == lastGroup 421 || !beforeFirstImport && isAlphabeticallySortableStaticImport(isStatic)) { 422 doVisitTokenInSameGroup(isStatic, previous, name, line); 423 } 424 else if (groupIdx > lastGroup) { 425 if (!beforeFirstImport && separated && line - lastImportLine < 2) { 426 log(line, MSG_SEPARATION, name); 427 } 428 } 429 else { 430 log(line, MSG_ORDERING, name); 431 } 432 433 lastGroup = groupIdx; 434 lastImport = name; 435 } 436 437 /** 438 * Checks whether static imports grouped by <b>top</b> or <b>bottom</b> option 439 * are sorted alphabetically or not. 440 * @param isStatic if current import is static. 441 * @return true if static imports should be sorted alphabetically. 442 */ 443 private boolean isAlphabeticallySortableStaticImport(boolean isStatic) { 444 return isStatic && sortStaticImportsAlphabetically 445 && (option == ImportOrderOption.TOP 446 || option == ImportOrderOption.BOTTOM); 447 } 448 449 /** 450 * Shares processing... 451 * 452 * @param isStatic whether the token is static or not. 453 * @param previous previous non-static but current is static (above), or 454 * previous static but current is non-static (under). 455 * @param name the name of the current import. 456 * @param line the line of the current import. 457 */ 458 private void doVisitTokenInSameGroup(boolean isStatic, 459 boolean previous, String name, int line) { 460 if (ordered) { 461 if (option == ImportOrderOption.INFLOW) { 462 // out of lexicographic order 463 if (compare(lastImport, name, caseSensitive) > 0) { 464 log(line, MSG_ORDERING, name); 465 } 466 } 467 else { 468 final boolean shouldFireError = 469 // previous non-static but current is static (above) 470 // or 471 // previous static but current is non-static (under) 472 previous 473 || 474 // current and previous static or current and 475 // previous non-static 476 lastImportStatic == isStatic 477 && 478 // and out of lexicographic order 479 compare(lastImport, name, caseSensitive) > 0; 480 481 if (shouldFireError) { 482 log(line, MSG_ORDERING, name); 483 } 484 } 485 } 486 } 487 488 /** 489 * Finds out what group the specified import belongs to. 490 * 491 * @param name the import name to find. 492 * @return group number for given import name. 493 */ 494 private int getGroupNumber(String name) { 495 int bestIndex = groups.length; 496 int bestLength = -1; 497 int bestPos = 0; 498 499 // find out what group this belongs in 500 // loop over groups and get index 501 for (int i = 0; i < groups.length; i++) { 502 final Matcher matcher = groups[i].matcher(name); 503 while (matcher.find()) { 504 final int length = matcher.end() - matcher.start(); 505 if (length > bestLength 506 || length == bestLength && matcher.start() < bestPos) { 507 bestIndex = i; 508 bestLength = length; 509 bestPos = matcher.start(); 510 } 511 } 512 } 513 514 return bestIndex; 515 } 516 517 /** 518 * Compares two strings. 519 * 520 * @param string1 521 * the first string. 522 * @param string2 523 * the second string. 524 * @param caseSensitive 525 * whether the comparison is case sensitive. 526 * @return the value {@code 0} if string1 is equal to string2; a value 527 * less than {@code 0} if string1 is lexicographically less 528 * than the string2; and a value greater than {@code 0} if 529 * string1 is lexicographically greater than string2. 530 */ 531 private static int compare(String string1, String string2, 532 boolean caseSensitive) { 533 final int result; 534 if (caseSensitive) { 535 result = string1.compareTo(string2); 536 } 537 else { 538 result = string1.compareToIgnoreCase(string2); 539 } 540 541 return result; 542 } 543}