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.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 }