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