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}