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}