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}