//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.checks.imports; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.FullIdent; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import com.puppycrawl.tools.checkstyle.utils.CommonUtils; /** * <ul> * <li>groups imports: ensures that groups of imports come in a specific order * (e.g., java. comes first, javax. comes second, then everything else)</li> * <li>adds a separation between groups : ensures that a blank line sit between * each group</li> * <li>import groups aren't separated internally: ensures that * each group aren't separated internally by blank line or comment</li> * <li>sorts imports inside each group: ensures that imports within each group * are in lexicographic order</li> * <li>sorts according to case: ensures that the comparison between import is * case sensitive</li> * <li>groups static imports: ensures that static imports are at the top (or the * bottom) of all the imports, or above (or under) each group, or are treated * like non static imports (@see {@link ImportOrderOption}</li> * </ul> * * <pre> * Properties: * </pre> * <table summary="Properties" border="1"> * <tr><th>name</th><th>Description</th><th>type</th><th>default value</th></tr> * <tr><td>option</td><td>policy on the relative order between regular imports and static * imports</td><td>{@link ImportOrderOption}</td><td>under</td></tr> * <tr><td>groups</td><td>list of imports groups (every group identified either by a common * prefix string, or by a regular expression enclosed in forward slashes (e.g. /regexp/)</td> * <td>list of strings</td><td>empty list</td></tr> * <tr><td>ordered</td><td>whether imports within group should be sorted</td> * <td>Boolean</td><td>true</td></tr> * <tr><td>separated</td><td>whether imports groups should be separated by, at least, * one blank line and aren't separated internally</td><td>Boolean</td><td>false</td></tr> * <tr><td>caseSensitive</td><td>whether string comparison should be case sensitive or not. * Case sensitive sorting is in ASCII sort order</td><td>Boolean</td><td>true</td></tr> * <tr><td>sortStaticImportsAlphabetically</td><td>whether static imports grouped by top or * bottom option are sorted alphabetically or not</td><td>Boolean</td><td>false</td></tr> * <tr><td>useContainerOrderingForStatic</td><td>whether to use container ordering * (Eclipse IDE term) for static imports or not</td><td>Boolean</td><td>false</td></tr> * </table> * * <p> * Example: * </p> * <p>To configure the check so that it matches default Eclipse formatter configuration * (tested on Kepler, Luna and Mars):</p> * <ul> * <li>group of static imports is on the top</li> * <li>groups of non-static imports: "java" then "javax" * packages first, then "org" and then all other imports</li> * <li>imports will be sorted in the groups</li> * <li>groups are separated by, at least, one blank line and aren't separated internally</li> * </ul> * * <pre> * <module name="ImportOrder"> * <property name="groups" value="/^javax?\./,org"/> * <property name="ordered" value="true"/> * <property name="separated" value="true"/> * <property name="option" value="above"/> * <property name="sortStaticImportsAlphabetically" value="true"/> * </module> * </pre> * * <p>To configure the check so that it matches default IntelliJ IDEA formatter configuration * (tested on v14):</p> * <ul> * <li>group of static imports is on the bottom</li> * <li>groups of non-static imports: all imports except of "javax" and * "java", then "javax" and "java"</li> * <li>imports will be sorted in the groups</li> * <li>groups are separated by, at least, one blank line and aren't separated internally</li> * </ul> * * <p> * Note: "separated" option is disabled because IDEA default has blank line * between "java" and static imports, and no blank line between * "javax" and "java" * </p> * * <pre> * <module name="ImportOrder"> * <property name="groups" value="*,javax,java"/> * <property name="ordered" value="true"/> * <property name="separated" value="false"/> * <property name="option" value="bottom"/> * <property name="sortStaticImportsAlphabetically" value="true"/> * </module> * </pre> * * <p>To configure the check so that it matches default NetBeans formatter configuration * (tested on v8):</p> * <ul> * <li>groups of non-static imports are not defined, all imports will be sorted * as a one group</li> * <li>static imports are not separated, they will be sorted along with other imports</li> * </ul> * * <pre> * <module name="ImportOrder"> * <property name="option" value="inflow"/> * </module> * </pre> * * <p> * Group descriptions enclosed in slashes are interpreted as regular * expressions. If multiple groups match, the one matching a longer * substring of the imported name will take precedence, with ties * broken first in favor of earlier matches and finally in favor of * the first matching group. * </p> * * <p> * There is always a wildcard group to which everything not in a named group * belongs. If an import does not match a named group, the group belongs to * this wildcard group. The wildcard group position can be specified using the * {@code *} character. * </p> * * <p>Check also has on option making it more flexible: * <b>sortStaticImportsAlphabetically</b> - sets whether static imports grouped by * <b>top</b> or <b>bottom</b> option should be sorted alphabetically or * not, default value is <b>false</b>. It is applied to static imports grouped * with <b>top</b> or <b>bottom</b> options.<br> * This option is helping in reconciling of this Check and other tools like * Eclipse's Organize Imports feature. * </p> * <p> * To configure the Check allows static imports grouped to the <b>top</b> * being sorted alphabetically: * </p> * * <pre> * {@code * import static java.lang.Math.abs; * import static org.abego.treelayout.Configuration.AlignmentInLevel; // OK, alphabetical order * * import org.abego.*; * * import java.util.Set; * * public class SomeClass { ... } * } * </pre> * * * @author Bill Schneider * @author o_sukhodolsky * @author David DIDIER * @author Steve McKay * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> * @author Andrei Selkin */ public class ImportOrderCheck extends AbstractCheck { /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_SEPARATION = "import.separation"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_ORDERING = "import.ordering"; /** * A key is pointing to the warning message text in "messages.properties" * file. */ public static final String MSG_SEPARATED_IN_GROUP = "import.groups.separated.internally"; /** The special wildcard that catches all remaining groups. */ private static final String WILDCARD_GROUP_NAME = "*"; /** Empty array of pattern type needed to initialize check. */ private static final Pattern[] EMPTY_PATTERN_ARRAY = new Pattern[0]; /** List of import groups specified by the user. */ private Pattern[] groups = EMPTY_PATTERN_ARRAY; /** Require imports in group be separated. */ private boolean separated; /** Require imports in group. */ private boolean ordered = true; /** Should comparison be case sensitive. */ private boolean caseSensitive = true; /** Last imported group. */ private int lastGroup; /** Line number of last import. */ private int lastImportLine; /** Name of last import. */ private String lastImport; /** If last import was static. */ private boolean lastImportStatic; /** Whether there was any imports. */ private boolean beforeFirstImport; /** Whether static imports should be sorted alphabetically or not. */ private boolean sortStaticImportsAlphabetically; /** Whether to use container ordering (Eclipse IDE term) for static imports or not. */ private boolean useContainerOrderingForStatic; /** The policy to enforce. */ private ImportOrderOption option = ImportOrderOption.UNDER; /** * Set the option to enforce. * @param optionStr string to decode option from * @throws IllegalArgumentException if unable to decode */ public void setOption(String optionStr) { try { option = ImportOrderOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH)); } catch (IllegalArgumentException iae) { throw new IllegalArgumentException("unable to parse " + optionStr, iae); } } /** * Sets the list of package groups and the order they should occur in the * file. * * @param packageGroups a comma-separated list of package names/prefixes. */ public void setGroups(String... packageGroups) { groups = new Pattern[packageGroups.length]; for (int i = 0; i < packageGroups.length; i++) { String pkg = packageGroups[i]; final Pattern grp; // if the pkg name is the wildcard, make it match zero chars // from any name, so it will always be used as last resort. if (WILDCARD_GROUP_NAME.equals(pkg)) { // matches any package grp = Pattern.compile(""); } else if (CommonUtils.startsWithChar(pkg, '/')) { if (!CommonUtils.endsWithChar(pkg, '/')) { throw new IllegalArgumentException("Invalid group"); } pkg = pkg.substring(1, pkg.length() - 1); grp = Pattern.compile(pkg); } else { final StringBuilder pkgBuilder = new StringBuilder(pkg); if (!CommonUtils.endsWithChar(pkg, '.')) { pkgBuilder.append('.'); } grp = Pattern.compile("^" + Pattern.quote(pkgBuilder.toString())); } groups[i] = grp; } } /** * Sets whether or not imports should be ordered within any one group of * imports. * * @param ordered * whether lexicographic ordering of imports within a group * required or not. */ public void setOrdered(boolean ordered) { this.ordered = ordered; } /** * Sets whether or not groups of imports must be separated from one another * by at least one blank line. * * @param separated * whether groups should be separated by oen blank line. */ public void setSeparated(boolean separated) { this.separated = separated; } /** * Sets whether string comparison should be case sensitive or not. * * @param caseSensitive * whether string comparison should be case sensitive. */ public void setCaseSensitive(boolean caseSensitive) { this.caseSensitive = caseSensitive; } /** * Sets whether static imports (when grouped using 'top' and 'bottom' option) * are sorted alphabetically or according to the package groupings. * @param sortAlphabetically true or false. */ public void setSortStaticImportsAlphabetically(boolean sortAlphabetically) { sortStaticImportsAlphabetically = sortAlphabetically; } /** * Sets whether to use container ordering (Eclipse IDE term) for static imports or not. * @param useContainerOrdering whether to use container ordering for static imports or not. */ public void setUseContainerOrderingForStatic(boolean useContainerOrdering) { useContainerOrderingForStatic = useContainerOrdering; } @Override public int[] getDefaultTokens() { return getAcceptableTokens(); } @Override public int[] getAcceptableTokens() { return new int[] {TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT}; } @Override public int[] getRequiredTokens() { return new int[] {TokenTypes.IMPORT}; } @Override public void beginTree(DetailAST rootAST) { lastGroup = Integer.MIN_VALUE; lastImportLine = Integer.MIN_VALUE; lastImport = ""; lastImportStatic = false; beforeFirstImport = true; } // -@cs[CyclomaticComplexity] SWITCH was transformed into IF-ELSE. @Override public void visitToken(DetailAST ast) { final FullIdent ident; final boolean isStatic; if (ast.getType() == TokenTypes.IMPORT) { ident = FullIdent.createFullIdentBelow(ast); isStatic = false; } else { ident = FullIdent.createFullIdent(ast.getFirstChild() .getNextSibling()); isStatic = true; } final boolean isStaticAndNotLastImport = isStatic && !lastImportStatic; final boolean isLastImportAndNonStatic = lastImportStatic && !isStatic; // using set of IF instead of SWITCH to analyze Enum options to satisfy coverage. // https://github.com/checkstyle/checkstyle/issues/1387 if (option == ImportOrderOption.TOP) { if (isLastImportAndNonStatic) { lastGroup = Integer.MIN_VALUE; lastImport = ""; } doVisitToken(ident, isStatic, isStaticAndNotLastImport); } else if (option == ImportOrderOption.BOTTOM) { if (isStaticAndNotLastImport) { lastGroup = Integer.MIN_VALUE; lastImport = ""; } doVisitToken(ident, isStatic, isLastImportAndNonStatic); } else if (option == ImportOrderOption.ABOVE) { // previous non-static but current is static doVisitToken(ident, isStatic, isStaticAndNotLastImport); } else if (option == ImportOrderOption.UNDER) { doVisitToken(ident, isStatic, isLastImportAndNonStatic); } else if (option == ImportOrderOption.INFLOW) { // "previous" argument is useless here doVisitToken(ident, isStatic, true); } else { throw new IllegalStateException( "Unexpected option for static imports: " + option); } lastImportLine = ast.findFirstToken(TokenTypes.SEMI).getLineNo(); lastImportStatic = isStatic; beforeFirstImport = false; } /** * Shares processing... * * @param ident the import to process. * @param isStatic whether the token is static or not. * @param previous previous non-static but current is static (above), or * previous static but current is non-static (under). */ private void doVisitToken(FullIdent ident, boolean isStatic, boolean previous) { final String name = ident.getText(); final int groupIdx = getGroupNumber(name); final int line = ident.getLineNo(); if (groupIdx == lastGroup || !beforeFirstImport && isAlphabeticallySortableStaticImport(isStatic)) { doVisitTokenInSameGroup(isStatic, previous, name, line); } else if (groupIdx > lastGroup) { if (!beforeFirstImport && separated && line - lastImportLine < 2) { log(line, MSG_SEPARATION, name); } } else { log(line, MSG_ORDERING, name); } if (checkSeparatorInGroup(groupIdx, isStatic, line)) { log(line, MSG_SEPARATED_IN_GROUP, name); } lastGroup = groupIdx; lastImport = name; } /** * Checks whether imports group separated internally. * @param groupIdx group number. * @param isStatic whether the token is static or not. * @param line the line of the current import. * @return true if imports group are separated internally. */ private boolean checkSeparatorInGroup(int groupIdx, boolean isStatic, int line) { return !beforeFirstImport && separated && groupIdx == lastGroup && isStatic == lastImportStatic && line - lastImportLine > 1; } /** * Checks whether static imports grouped by <b>top</b> or <b>bottom</b> option * are sorted alphabetically or not. * @param isStatic if current import is static. * @return true if static imports should be sorted alphabetically. */ private boolean isAlphabeticallySortableStaticImport(boolean isStatic) { return isStatic && sortStaticImportsAlphabetically && (option == ImportOrderOption.TOP || option == ImportOrderOption.BOTTOM); } /** * Shares processing... * * @param isStatic whether the token is static or not. * @param previous previous non-static but current is static (above), or * previous static but current is non-static (under). * @param name the name of the current import. * @param line the line of the current import. */ private void doVisitTokenInSameGroup(boolean isStatic, boolean previous, String name, int line) { if (ordered) { if (option == ImportOrderOption.INFLOW) { if (isWrongOrder(name, isStatic)) { log(line, MSG_ORDERING, name); } } else { final boolean shouldFireError = // previous non-static but current is static (above) // or // previous static but current is non-static (under) previous || // current and previous static or current and // previous non-static lastImportStatic == isStatic && isWrongOrder(name, isStatic); if (shouldFireError) { log(line, MSG_ORDERING, name); } } } } /** * Checks whether import name is in wrong order. * @param name import name. * @param isStatic whether it is a static import name. * @return true if import name is in wrong order. */ private boolean isWrongOrder(String name, boolean isStatic) { final boolean result; if (isStatic && useContainerOrderingForStatic) { result = compareContainerOrder(lastImport, name, caseSensitive) > 0; } else { // out of lexicographic order result = compare(lastImport, name, caseSensitive) > 0; } return result; } /** * Compares two import strings. * We first compare the container of the static import, container being the type enclosing * the static element being imported. When this returns 0, we compare the qualified * import name. For e.g. this is what is considered to be container names: * <p> * import static HttpConstants.COLON => HttpConstants * import static HttpHeaders.addHeader => HttpHeaders * import static HttpHeaders.setHeader => HttpHeaders * import static HttpHeaders.Names.DATE => HttpHeaders.Names * </p> * <p> * According to this logic, HttpHeaders.Names would come after HttpHeaders. * * For more details, see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=473629#c3"> * static imports comparison method</a> in Eclipse. * </p> * * @param importName1 first import name. * @param importName2 second import name. * @param caseSensitive whether the comparison of fully qualified import names is case * sensitive. * @return the value {@code 0} if str1 is equal to str2; a value * less than {@code 0} if str is less than the str2 (container order * or lexicographical); and a value greater than {@code 0} if str1 is greater than str2 * (container order or lexicographically). */ private static int compareContainerOrder(String importName1, String importName2, boolean caseSensitive) { final String container1 = getImportContainer(importName1); final String container2 = getImportContainer(importName2); final int compareContainersOrderResult; if (caseSensitive) { compareContainersOrderResult = container1.compareTo(container2); } else { compareContainersOrderResult = container1.compareToIgnoreCase(container2); } final int result; if (compareContainersOrderResult == 0) { result = compare(importName1, importName2, caseSensitive); } else { result = compareContainersOrderResult; } return result; } /** * Extracts import container name from fully qualified import name. * An import container name is the type which encloses the static element being imported. * For example, HttpConstants, HttpHeaders, HttpHeaders.Names are import container names: * <p> * import static HttpConstants.COLON => HttpConstants * import static HttpHeaders.addHeader => HttpHeaders * import static HttpHeaders.setHeader => HttpHeaders * import static HttpHeaders.Names.DATE => HttpHeaders.Names * </p> * @param qualifiedImportName fully qualified import name. * @return import container name. */ private static String getImportContainer(String qualifiedImportName) { final int lastDotIndex = qualifiedImportName.lastIndexOf('.'); return qualifiedImportName.substring(0, lastDotIndex); } /** * Finds out what group the specified import belongs to. * * @param name the import name to find. * @return group number for given import name. */ private int getGroupNumber(String name) { int bestIndex = groups.length; int bestLength = -1; int bestPos = 0; // find out what group this belongs in // loop over groups and get index for (int i = 0; i < groups.length; i++) { final Matcher matcher = groups[i].matcher(name); while (matcher.find()) { final int length = matcher.end() - matcher.start(); if (length > bestLength || length == bestLength && matcher.start() < bestPos) { bestIndex = i; bestLength = length; bestPos = matcher.start(); } } } return bestIndex; } /** * Compares two strings. * * @param string1 * the first string. * @param string2 * the second string. * @param caseSensitive * whether the comparison is case sensitive. * @return the value {@code 0} if string1 is equal to string2; a value * less than {@code 0} if string1 is lexicographically less * than the string2; and a value greater than {@code 0} if * string1 is lexicographically greater than string2. */ private static int compare(String string1, String string2, boolean caseSensitive) { final int result; if (caseSensitive) { result = string1.compareTo(string2); } else { result = string1.compareToIgnoreCase(string2); } return result; } }