1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.imports;
21
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Locale;
25
26 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28 import com.puppycrawl.tools.checkstyle.api.DetailAST;
29 import com.puppycrawl.tools.checkstyle.api.FullIdent;
30 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31
32 /**
33 * <div>
34 * Checks the ordering and placement of module import declarations. Features are:
35 * </div>
36 * <ul>
37 * <li>
38 * position of module imports: ensures that module imports are placed above or below
39 * all type and static imports (see
40 * <a href="https://checkstyle.org/property_types.html#ModuleImportOrderOption">
41 * ModuleImportOrderOption</a>)
42 * </li>
43 * <li>
44 * sorts module imports: ensures that module imports are sorted lexicographically
45 * by qualified module name, in
46 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
47 * </li>
48 * <li>
49 * adds a separation between module imports and other imports: ensures that the module
50 * import block is separated from type and static imports by, at least, one blank
51 * line or comment
52 * </li>
53 * </ul>
54 *
55 * <p>
56 * This check only validates module imports. It observes type and static imports to
57 * locate the boundary of the module import block, but does not validate their order.
58 * Use {@code ImportOrder} alongside this check for those.
59 * </p>
60 *
61 * @since 14.1.0
62 */
63 @FileStatefulCheck
64 public class ModuleImportOrderCheck extends AbstractCheck {
65
66 /**
67 * A key pointing to the warning message text in "messages.properties" file.
68 * Emitted when a module import is not placed above or below all type and
69 * static imports, as required by the configured option.
70 */
71 public static final String MSG_POSITION = "module.import.position";
72
73 /**
74 * A key pointing to the warning message text in "messages.properties" file.
75 * Emitted when the module import block is not separated from type and
76 * static imports by a blank line.
77 */
78 public static final String MSG_SEPARATION = "module.import.separation";
79
80 /**
81 * A key pointing to the warning message text in "messages.properties" file.
82 * Emitted when module imports are not sorted lexicographically by
83 * qualified module name.
84 */
85 public static final String MSG_ORDERING_LEX = "module.import.ordering.lex";
86
87 /** Imports of the current file in order of appearance. */
88 private final List<ImportEntry> imports = new ArrayList<>();
89
90 /**
91 * Specify policy on the position of module imports relative to type and
92 * static imports.
93 */
94 private ModuleImportOrderOption option = ModuleImportOrderOption.TOP;
95
96 /**
97 * Control whether the module import block should be separated from type and
98 * static imports by, at least, one blank line or comment.
99 */
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 }