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