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.modules;
21
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Set;
25
26 import com.puppycrawl.tools.checkstyle.StatelessCheck;
27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28 import com.puppycrawl.tools.checkstyle.api.DetailAST;
29 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30 import com.puppycrawl.tools.checkstyle.internal.annotation.PreserveOrder;
31 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
32
33 /**
34 * <div>
35 * Checks the ordering, grouping and separation of directives in a module
36 * declaration. Directives of each kind must form a single block, the blocks
37 * must appear in a configurable order, and each block must be separated from
38 * the previous one by exactly one blank line.
39 * </div>
40 *
41 * <p>
42 * The default configuration enforces
43 * <a href="https://google.github.io/styleguide/javaguide.html#s3.5-module-declaration">
44 * Google Java Style Guide, Section 3.5.1</a>: all {@code requires} directives
45 * first, then {@code exports}, {@code opens}, {@code uses} and {@code provides},
46 * each kind in a single block, with a single blank line between blocks. Blank
47 * lines are what delimit blocks, so blank lines between directives of the same
48 * kind are also violations.
49 * </p>
50 *
51 * <p>
52 * All forms of {@code requires} (plain, {@code transitive}, {@code static})
53 * belong to a single block, and the order of directives inside a block is not
54 * validated.
55 * </p>
56 *
57 * <p>
58 * Directive kinds that are not listed in the {@code order} property are not
59 * validated.
60 * </p>
61 *
62 * @since 14.1.0
63 */
64 @StatelessCheck
65 public class ModuleDirectiveOrderCheck extends AbstractCheck {
66
67 /**
68 * A key pointing to the warning message text in "messages.properties" file.
69 * Emitted when a directive block appears after a block that it should precede.
70 */
71 public static final String MSG_ORDER = "module.directive.order";
72
73 /**
74 * A key pointing to the warning message text in "messages.properties" file.
75 * Emitted when directives of one kind are interleaved with directives of
76 * another kind.
77 */
78 public static final String MSG_GROUPING = "module.directive.grouping";
79
80 /**
81 * A key pointing to the warning message text in "messages.properties" file.
82 * Emitted when directives of the same kind are separated by blank lines.
83 */
84 public static final String MSG_SEPARATED_INTERNALLY =
85 "module.directive.separated.internally";
86
87 /**
88 * A key pointing to the warning message text in "messages.properties" file.
89 * Emitted when a directive block is not separated from the previous block
90 * by exactly one blank line.
91 */
92 public static final String MSG_SEPARATION = "module.directive.separation";
93
94 /** Default order of directive kinds. */
95 private static final List<String> DEFAULT_ORDER = List.of(
96 "requires",
97 "exports",
98 "opens",
99 "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 }