001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.ArrayList;
023import java.util.List;
024import java.util.Locale;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.FullIdent;
030import com.puppycrawl.tools.checkstyle.api.TokenTypes;
031
032/**
033 * <div>
034 * Checks the ordering and placement of module import declarations. Features are:
035 * </div>
036 * <ul>
037 * <li>
038 * position of module imports: ensures that module imports are placed above or below
039 * all type and static imports (see
040 * <a href="https://checkstyle.org/property_types.html#ModuleImportOrderOption">
041 * ModuleImportOrderOption</a>)
042 * </li>
043 * <li>
044 * sorts module imports: ensures that module imports are sorted lexicographically
045 * by qualified module name, in
046 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
047 * </li>
048 * <li>
049 * adds a separation between module imports and other imports: ensures that the module
050 * import block is separated from type and static imports by, at least, one blank
051 * line or comment
052 * </li>
053 * </ul>
054 *
055 * <p>
056 * This check only validates module imports. It observes type and static imports to
057 * locate the boundary of the module import block, but does not validate their order.
058 * Use {@code ImportOrder} alongside this check for those.
059 * </p>
060 *
061 * @since 14.1.0
062 */
063@FileStatefulCheck
064public class ModuleImportOrderCheck extends AbstractCheck {
065
066    /**
067     * A key pointing to the warning message text in "messages.properties" file.
068     * Emitted when a module import is not placed above or below all type and
069     * static imports, as required by the configured option.
070     */
071    public static final String MSG_POSITION = "module.import.position";
072
073    /**
074     * A key pointing to the warning message text in "messages.properties" file.
075     * Emitted when the module import block is not separated from type and
076     * static imports by a blank line.
077     */
078    public static final String MSG_SEPARATION = "module.import.separation";
079
080    /**
081     * A key pointing to the warning message text in "messages.properties" file.
082     * Emitted when module imports are not sorted lexicographically by
083     * qualified module name.
084     */
085    public static final String MSG_ORDERING_LEX = "module.import.ordering.lex";
086
087    /** Imports of the current file in order of appearance. */
088    private final List<ImportEntry> imports = new ArrayList<>();
089
090    /**
091     * Specify policy on the position of module imports relative to type and
092     * static imports.
093     */
094    private ModuleImportOrderOption option = ModuleImportOrderOption.TOP;
095
096    /**
097     * Control whether the module import block should be separated from type and
098     * static imports by, at least, one blank line or comment.
099     */
100    private boolean separated;
101
102    /**
103     * Creates a new {@code ModuleImportOrderCheck} instance.
104     */
105    public ModuleImportOrderCheck() {
106        // no code by default
107    }
108
109    /**
110     * Setter to specify policy on the position of module imports relative to type
111     * and static imports.
112     *
113     * @param optionStr string to decode option from
114     * @throws IllegalArgumentException if unable to decode
115     * @since 14.1.0
116     */
117    public void setOption(String optionStr) {
118        option = ModuleImportOrderOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
119    }
120
121    /**
122     * Setter to control whether the module import block should be separated from
123     * type and static imports by, at least, one blank line or comment.
124     *
125     * @param separated whether the module import block should be separated.
126     * @since 14.1.0
127     */
128    public void setSeparated(boolean separated) {
129        this.separated = separated;
130    }
131
132    @Override
133    public int[] getDefaultTokens() {
134        return getRequiredTokens();
135    }
136
137    @Override
138    public int[] getAcceptableTokens() {
139        return getRequiredTokens();
140    }
141
142    @Override
143    public int[] getRequiredTokens() {
144        return new int[] {
145            TokenTypes.IMPORT,
146            TokenTypes.STATIC_IMPORT,
147            TokenTypes.MODULE_IMPORT,
148        };
149    }
150
151    @Override
152    public void beginTree(DetailAST rootAST) {
153        imports.clear();
154    }
155
156    @Override
157    public void visitToken(DetailAST ast) {
158        final FullIdent ident;
159        if (ast.getType() == TokenTypes.IMPORT) {
160            ident = FullIdent.createFullIdentBelow(ast);
161        }
162        else {
163            ident = FullIdent.createFullIdent(ast.getFirstChild().getNextSibling());
164        }
165        imports.add(new ImportEntry(ident.getText(),
166                ast.getType() == TokenTypes.MODULE_IMPORT, ast));
167    }
168
169    @Override
170    public void finishTree(DetailAST rootAST) {
171        checkLexicographicalOrder();
172        final boolean misplaced = checkPosition();
173        if (separated && !misplaced) {
174            checkSeparation();
175        }
176    }
177
178    /**
179     * Checks that module imports are sorted lexicographically. Each module import
180     * is compared with the previous module import, regardless of any type or
181     * static imports between them.
182     */
183    private void checkLexicographicalOrder() {
184        String previousModule = null;
185        for (final ImportEntry entry : imports) {
186            if (entry.module()) {
187                if (previousModule != null && previousModule.compareTo(entry.name()) > 0) {
188                    log(entry.ast(), MSG_ORDERING_LEX, entry.name(), previousModule);
189                }
190                previousModule = entry.name();
191            }
192        }
193    }
194
195    /**
196     * Checks that module imports are placed above or below all type and static
197     * imports, according to the configured option.
198     *
199     * @return true if any position violation was logged.
200     */
201    private boolean checkPosition() {
202        boolean violation = false;
203        boolean seenNonModule = false;
204        if (option == ModuleImportOrderOption.TOP) {
205            for (final ImportEntry entry : imports) {
206                if (seenNonModule && entry.module()) {
207                    log(entry.ast(), MSG_POSITION, entry.name());
208                    violation = true;
209                }
210                seenNonModule = seenNonModule || !entry.module();
211            }
212        }
213        else {
214            for (int index = imports.size() - 1; index >= 0; index--) {
215                final ImportEntry entry = imports.get(index);
216                if (seenNonModule && entry.module()) {
217                    log(entry.ast(), MSG_POSITION, entry.name());
218                    violation = true;
219                }
220                seenNonModule = seenNonModule || !entry.module();
221            }
222        }
223        return violation;
224    }
225
226    /**
227     * Checks that the module import block is separated from the adjacent type and
228     * static import block by, at least, one blank line or comment. This method is
229     * only invoked when module imports are correctly positioned, so all module
230     * imports form a single block above or below all other imports.
231     */
232    private void checkSeparation() {
233        int lastModuleIndex = -1;
234        int firstModuleIndex = -1;
235        for (int index = 0; index < imports.size(); index++) {
236            if (imports.get(index).module()) {
237                if (firstModuleIndex == -1) {
238                    firstModuleIndex = index;
239                }
240                lastModuleIndex = index;
241            }
242        }
243
244        final int boundaryIndex;
245        if (option == ModuleImportOrderOption.TOP) {
246            boundaryIndex = lastModuleIndex + 1;
247        }
248        else {
249            boundaryIndex = firstModuleIndex;
250        }
251
252        if (boundaryIndex > 0 && boundaryIndex < imports.size()) {
253            final ImportEntry boundary = imports.get(boundaryIndex);
254            final ImportEntry previous = imports.get(boundaryIndex - 1);
255            if (boundary.getStartLineNumber() - previous.getEndLineNumber() < 2) {
256                log(boundary.ast(), MSG_SEPARATION, boundary.name());
257            }
258        }
259    }
260
261    /**
262     * Contains import attributes as import full path, module flag and import AST.
263     *
264     * @param name fully qualified name of the import
265     * @param module whether the import is a module import
266     * @param ast import AST
267     */
268    private record ImportEntry(String name, boolean module, DetailAST ast) {
269
270        /**
271         * Get import start line number from ast.
272         *
273         * @return import start line from ast.
274         */
275        /* package */ int getStartLineNumber() {
276            return ast.getLineNo();
277        }
278
279        /**
280         * Get import end line number from ast.
281         *
282         * <p>
283         * <b>Note:</b> It can be different from <b>startLineNumber</b> when import
284         * statement spans multiple lines.
285         * </p>
286         *
287         * @return import end line from ast.
288         */
289        /* package */ int getEndLineNumber() {
290            return ast.getLastChild().getLineNo();
291        }
292    }
293
294}