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