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.modules; 021 022import java.util.HashSet; 023import java.util.List; 024import java.util.Set; 025 026import com.puppycrawl.tools.checkstyle.StatelessCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.internal.annotation.PreserveOrder; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 032 033/** 034 * <div> 035 * Checks the ordering, grouping and separation of directives in a module 036 * declaration. Directives of each kind must form a single block, the blocks 037 * must appear in a configurable order, and each block must be separated from 038 * the previous one by exactly one blank line. 039 * </div> 040 * 041 * <p> 042 * The default configuration enforces 043 * <a href="https://google.github.io/styleguide/javaguide.html#s3.5-module-declaration"> 044 * Google Java Style Guide, Section 3.5.1</a>: all {@code requires} directives 045 * first, then {@code exports}, {@code opens}, {@code uses} and {@code provides}, 046 * each kind in a single block, with a single blank line between blocks. Blank 047 * lines are what delimit blocks, so blank lines between directives of the same 048 * kind are also violations. 049 * </p> 050 * 051 * <p> 052 * All forms of {@code requires} (plain, {@code transitive}, {@code static}) 053 * belong to a single block, and the order of directives inside a block is not 054 * validated. 055 * </p> 056 * 057 * <p> 058 * Directive kinds that are not listed in the {@code order} property are not 059 * validated. 060 * </p> 061 * 062 * @since 14.1.0 063 */ 064@StatelessCheck 065public class ModuleDirectiveOrderCheck extends AbstractCheck { 066 067 /** 068 * A key pointing to the warning message text in "messages.properties" file. 069 * Emitted when a directive block appears after a block that it should precede. 070 */ 071 public static final String MSG_ORDER = "module.directive.order"; 072 073 /** 074 * A key pointing to the warning message text in "messages.properties" file. 075 * Emitted when directives of one kind are interleaved with directives of 076 * another kind. 077 */ 078 public static final String MSG_GROUPING = "module.directive.grouping"; 079 080 /** 081 * A key pointing to the warning message text in "messages.properties" file. 082 * Emitted when directives of the same kind are separated by blank lines. 083 */ 084 public static final String MSG_SEPARATED_INTERNALLY = 085 "module.directive.separated.internally"; 086 087 /** 088 * A key pointing to the warning message text in "messages.properties" file. 089 * Emitted when a directive block is not separated from the previous block 090 * by exactly one blank line. 091 */ 092 public static final String MSG_SEPARATION = "module.directive.separation"; 093 094 /** Default order of directive kinds. */ 095 private static final List<String> DEFAULT_ORDER = List.of( 096 "requires", 097 "exports", 098 "opens", 099 "uses", 100 "provides" 101 ); 102 103 /** Valid values for entries of the {@code order} property. */ 104 private static final Set<String> VALID_KINDS = Set.copyOf(DEFAULT_ORDER); 105 106 /** 107 * Specify directive kinds in the order their blocks must appear inside 108 * the module declaration. 109 */ 110 @PreserveOrder 111 private List<String> order = DEFAULT_ORDER; 112 113 /** 114 * Control whether blank line separation is validated: exactly one blank 115 * line between directive blocks and no blank lines inside a block. 116 */ 117 private boolean validateBlockSeparation = true; 118 119 /** 120 * Creates a new {@code ModuleDirectiveOrderCheck} instance. 121 */ 122 public ModuleDirectiveOrderCheck() { 123 // no code by default 124 } 125 126 /** 127 * Setter to specify directive kinds in the order their blocks must appear 128 * inside the module declaration. 129 * 130 * @param order the order of directive kinds. 131 * @throws IllegalArgumentException when an element of order is not a 132 * directive kind. 133 * @since 14.1.0 134 */ 135 public void setOrder(String... order) { 136 for (final String kind : order) { 137 if (!VALID_KINDS.contains(kind)) { 138 throw new IllegalArgumentException("unable to parse " + kind); 139 } 140 } 141 this.order = List.of(order); 142 } 143 144 /** 145 * Setter to control whether blank line separation is validated: exactly 146 * one blank line between directive blocks and no blank lines inside a block. 147 * 148 * @param validateBlockSeparation the value to set. 149 * @since 14.1.0 150 */ 151 public void setValidateBlockSeparation(boolean validateBlockSeparation) { 152 this.validateBlockSeparation = validateBlockSeparation; 153 } 154 155 @Override 156 public int[] getDefaultTokens() { 157 return getRequiredTokens(); 158 } 159 160 @Override 161 public int[] getAcceptableTokens() { 162 return getRequiredTokens(); 163 } 164 165 @Override 166 public int[] getRequiredTokens() { 167 return new int[] {TokenTypes.MODULE_DEF}; 168 } 169 170 @Override 171 public void visitToken(DetailAST ast) { 172 final DetailAST directiveBlock = ast.findFirstToken(TokenTypes.DIRECTIVE_BLOCK); 173 final Set<String> seenKinds = new HashSet<>(); 174 DetailAST previous = null; 175 for (DetailAST child = directiveBlock.getFirstChild(); child != null; 176 child = child.getNextSibling()) { 177 if (order.contains(child.getText())) { 178 if (previous != null) { 179 validateDirectivePlacement(child, previous, seenKinds); 180 } 181 seenKinds.add(child.getText()); 182 previous = child; 183 } 184 } 185 } 186 187 /** 188 * Validates the placement of a directive relative to the previous directive 189 * of the module. 190 * 191 * <p> 192 * A directive of the same kind as the previous one continues the current 193 * block and must not be separated from it by blank lines. Otherwise the 194 * directive starts a new block, which must not repeat an earlier kind, 195 * must not belong before the previous block, and must be separated from 196 * it by exactly one blank line. Blank line requirements are validated 197 * only when {@code validateBlockSeparation} is enabled. 198 * </p> 199 * 200 * @param directive the directive to validate. 201 * @param previous the directive before the given one. 202 * @param seenKinds kinds of all directives seen before the given one. 203 */ 204 private void validateDirectivePlacement(DetailAST directive, DetailAST previous, 205 Set<String> seenKinds) { 206 final String kind = directive.getText(); 207 final String previousKind = previous.getText(); 208 final int blankLines = countBlankLinesBetweenDirectives(previous, directive); 209 if (kind.equals(previousKind)) { 210 if (validateBlockSeparation && blankLines > 0) { 211 log(directive, MSG_SEPARATED_INTERNALLY, kind); 212 } 213 } 214 else if (seenKinds.contains(kind)) { 215 log(directive, MSG_GROUPING, kind); 216 } 217 else if (precedesInOrder(kind, previousKind)) { 218 log(directive, MSG_ORDER, kind, previousKind); 219 } 220 else if (validateBlockSeparation && blankLines != 1) { 221 log(directive, MSG_SEPARATION, kind); 222 } 223 } 224 225 /** 226 * Checks whether the given kind precedes the other kind in the {@code order} property. 227 * 228 * @param kind the kind of the directive being validated. 229 * @param previousKind the kind of the previous directive. 230 * @return true if {@code kind} precedes {@code previousKind} in the configured order. 231 */ 232 private boolean precedesInOrder(String kind, String previousKind) { 233 return order.stream() 234 .takeWhile(entry -> !entry.equals(previousKind)) 235 .anyMatch(kind::equals); 236 } 237 238 /** 239 * Counts the blank lines between the end of the previous directive and 240 * the start of the given directive. 241 * 242 * @param previous the directive before the given one. 243 * @param directive the directive to count blank lines before. 244 * @return the number of blank lines between the two directives. 245 */ 246 private int countBlankLinesBetweenDirectives(DetailAST previous, DetailAST directive) { 247 final int previousEnd = previous.getLastChild().getLineNo(); 248 final int directiveStart = directive.getLineNo(); 249 int result = 0; 250 for (int lineIndex = previousEnd; lineIndex <= directiveStart - 2; lineIndex++) { 251 if (CommonUtil.isBlank(getLine(lineIndex))) { 252 result++; 253 } 254 } 255 return result; 256 } 257 258}