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: &quot;java&quot; then &quot;javax&quot;
074 *         packages first, then &quot;org&quot; 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 * &lt;module name=&quot;ImportOrder&quot;&gt;
081 *    &lt;property name=&quot;groups&quot; value=&quot;/^javax?\./,org&quot;/&gt;
082 *    &lt;property name=&quot;ordered&quot; value=&quot;true&quot;/&gt;
083 *    &lt;property name=&quot;separated&quot; value=&quot;true&quot;/&gt;
084 *    &lt;property name=&quot;option&quot; value=&quot;above&quot;/&gt;
085 *    &lt;property name=&quot;sortStaticImportsAlphabetically&quot; value=&quot;true&quot;/&gt;
086 * &lt;/module&gt;
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 &quot;javax&quot; and
094 *         &quot;java&quot;, then &quot;javax&quot; and &quot;java&quot;</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: &quot;separated&quot; option is disabled because IDEA default has blank line
101 *         between &quot;java&quot; and static imports, and no blank line between
102 *         &quot;javax&quot; and &quot;java&quot;
103 *         </p>
104 *
105 * <pre>
106 * &lt;module name=&quot;ImportOrder&quot;&gt;
107 *     &lt;property name=&quot;groups&quot; value=&quot;*,javax,java&quot;/&gt;
108 *     &lt;property name=&quot;ordered&quot; value=&quot;true&quot;/&gt;
109 *     &lt;property name=&quot;separated&quot; value=&quot;false&quot;/&gt;
110 *     &lt;property name=&quot;option&quot; value=&quot;bottom&quot;/&gt;
111 *     &lt;property name=&quot;sortStaticImportsAlphabetically&quot; value=&quot;true&quot;/&gt;
112 * &lt;/module&gt;
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 * &lt;module name=&quot;ImportOrder&quot;&gt;
125 *     &lt;property name=&quot;option&quot; value=&quot;inflow&quot;/&gt;
126 * &lt;/module&gt;
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}