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