View Javadoc
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 }