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.javadoc;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027
028import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.DetailNode;
031import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
034import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
035import com.puppycrawl.tools.checkstyle.utils.NullUtil;
036import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
037
038/**
039 * <div>
040 * Checks that {@code @param} tags in Javadoc comments are in the same order as the
041 * parameters in the declaration.
042 * </div>
043 *
044 * <p>
045 * Type parameters must come before regular parameters. For record declarations, record
046 * components are treated as regular parameters and must be documented after type parameters.
047 * For compact constructors, the expected parameter order is the order of the record components
048 * in the record declaration.
049 * </p>
050 *
051 * <p>
052 * The check does not validate missing, extra, or duplicate {@code @param} tags. It reports only
053 * tags that move backward in the declaration order.
054 * </p>
055 *
056 * @since 14.1.0
057 */
058@FileStatefulCheck
059public class JavadocParamOrderCheck extends AbstractJavadocCheck {
060
061    /**
062     * A key is pointing to the warning message text in "messages.properties" file.
063     */
064    public static final String MSG_KEY = "javadoc.param.order";
065
066    /** Html element start symbol. */
067    private static final String ELEMENT_START = "<";
068
069    /** Html element end symbol. */
070    private static final String ELEMENT_END = ">";
071
072    /** Javadoc param tag names, mapped by their corresponding Javadoc node. */
073    private final Map<DetailNode, String> javadocTags = new LinkedHashMap<>();
074
075    /**
076     * Creates a new {@code JavadocParamOrderCheck} instance.
077     */
078    public JavadocParamOrderCheck() {
079        // no code by default
080    }
081
082    @Override
083    public int[] getDefaultTokens() {
084        return getRequiredTokens();
085    }
086
087    @Override
088    public final int[] getRequiredTokens() {
089        return new int[] {
090            TokenTypes.METHOD_DEF,
091            TokenTypes.CTOR_DEF,
092            TokenTypes.CLASS_DEF,
093            TokenTypes.INTERFACE_DEF,
094            TokenTypes.COMPACT_CTOR_DEF,
095            TokenTypes.RECORD_DEF,
096        };
097    }
098
099    @Override
100    public int[] getDefaultJavadocTokens() {
101        return getRequiredJavadocTokens();
102    }
103
104    @Override
105    public int[] getRequiredJavadocTokens() {
106        return new int[] {
107            JavadocCommentsTokenTypes.PARAM_BLOCK_TAG,
108        };
109    }
110
111    @Override
112    public final void visitToken(final DetailAST ast) {
113        final DetailAST blockCommentNode = JavadocUtil.getAttachedJavadocComment(ast);
114        if (blockCommentNode != null) {
115            javadocTags.clear();
116            super.visitToken(blockCommentNode);
117            for (Map.Entry<DetailNode, String> javadocTag : getMisorderedParamTags(ast)) {
118                log(javadocTag.getKey(), MSG_KEY, javadocTag.getValue());
119            }
120        }
121    }
122
123    @Override
124    public void visitJavadocToken(final DetailNode ast) {
125        collectParam(ast);
126    }
127
128    /**
129     * Collects a param tag.
130     *
131     * @param ast the param tag node
132     */
133    private void collectParam(final DetailNode ast) {
134        final DetailNode parameterName = JavadocUtil.findFirstToken(
135                ast, JavadocCommentsTokenTypes.PARAMETER_NAME);
136        if (parameterName != null) {
137            javadocTags.put(ast, parameterName.getText());
138        }
139    }
140
141    /**
142     * Gets collected Javadoc param tags that violate the expected declaration order.
143     *
144     * @param ast Java AST node whose Javadoc is being checked
145     * @return collected Javadoc param tags that violate the expected declaration order
146     */
147    private List<Map.Entry<DetailNode, String>> getMisorderedParamTags(final DetailAST ast) {
148        final List<String> expectedParamOrder = getExpectedParamOrder(ast);
149        final List<Map.Entry<DetailNode, String>> misorderedTags = new ArrayList<>();
150
151        int maxIndexOfPreviousParam = -1;
152        for (Map.Entry<DetailNode, String> javadocTag : javadocTags.entrySet()) {
153            final int currentIndex = expectedParamOrder.indexOf(javadocTag.getValue());
154
155            if (currentIndex >= 0) {
156                if (currentIndex < maxIndexOfPreviousParam) {
157                    misorderedTags.add(javadocTag);
158                }
159                else {
160                    maxIndexOfPreviousParam = currentIndex;
161                }
162            }
163        }
164        return misorderedTags;
165    }
166
167    /**
168     * Gets expected param tag order for the current AST node.
169     *
170     * @param ast Java AST node whose Javadoc is being checked
171     * @return expected param tag order
172     */
173    private static List<String> getExpectedParamOrder(final DetailAST ast) {
174        final List<String> expectedParamOrder = new ArrayList<>();
175
176        addTypeParameterNames(expectedParamOrder, ast);
177
178        switch (ast.getType()) {
179            case TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF ->
180                addParameterNames(expectedParamOrder, ast);
181
182            case TokenTypes.RECORD_DEF ->
183                addRecordComponentNames(expectedParamOrder, ast);
184
185            case TokenTypes.COMPACT_CTOR_DEF ->
186                addRecordComponentNames(expectedParamOrder, getRecordDef(ast));
187
188            default -> {
189                // No formal parameters for type definitions other than records.
190            }
191        }
192
193        return expectedParamOrder;
194    }
195
196    /**
197     * Adds type parameter names from the supplied AST node.
198     *
199     * @param paramNames destination list
200     * @param ast node to inspect
201     */
202    private static void addTypeParameterNames(final Collection<String> paramNames,
203            final DetailAST ast) {
204        for (String typeParamName : CheckUtil.getTypeParameterNames(ast)) {
205            paramNames.add(ELEMENT_START + typeParamName + ELEMENT_END);
206        }
207    }
208
209    /**
210     * Adds parameter names from the supplied method or constructor AST node.
211     *
212     * @param paramNames destination list
213     * @param ast node to inspect
214     */
215    private static void addParameterNames(final Collection<String> paramNames,
216            final DetailAST ast) {
217        final DetailAST parameters = NullUtil.notNull(ast.findFirstToken(TokenTypes.PARAMETERS));
218        TokenUtil.forEachChild(parameters, TokenTypes.PARAMETER_DEF,
219                paramDef -> addParameterName(paramNames, paramDef));
220    }
221
222    /**
223     * Adds a parameter name from the supplied parameter definition AST node.
224     *
225     * @param paramNames destination list
226     * @param paramDef parameter definition node
227     */
228    private static void addParameterName(final Collection<String> paramNames,
229            final DetailAST paramDef) {
230        if (!CheckUtil.isReceiverParameter(paramDef)) {
231            final DetailAST ident = NullUtil.notNull(paramDef.findFirstToken(TokenTypes.IDENT));
232            paramNames.add(ident.getText());
233        }
234    }
235
236    /**
237     * Adds record component names from the supplied record AST node.
238     *
239     * @param paramNames destination list
240     * @param recordDef record definition node
241     */
242    private static void addRecordComponentNames(final Collection<String> paramNames,
243            final DetailAST recordDef) {
244        for (DetailAST component : getRecordComponents(recordDef)) {
245            paramNames.add(component.getText());
246        }
247    }
248
249    /**
250     * Finds the nearest ancestor record definition node for the given AST node.
251     *
252     * @param ast the AST node to start searching from
253     * @return the nearest {@code RECORD_DEF} AST node
254     */
255    private static DetailAST getRecordDef(final DetailAST ast) {
256        DetailAST current = ast;
257        while (current.getType() != TokenTypes.RECORD_DEF) {
258            current = current.getParent();
259        }
260        return current;
261    }
262
263    /**
264     * Gets record component identifier nodes from a record definition.
265     *
266     * @param recordDef record definition node
267     * @return record component identifier nodes
268     */
269    private static List<DetailAST> getRecordComponents(final DetailAST recordDef) {
270        final List<DetailAST> components = new ArrayList<>();
271        final DetailAST recordDecl = NullUtil.notNull(
272                recordDef.findFirstToken(TokenTypes.RECORD_COMPONENTS));
273
274        DetailAST child = recordDecl.getFirstChild();
275        while (child != null) {
276            if (child.getType() == TokenTypes.RECORD_COMPONENT_DEF) {
277                components.add(NullUtil.notNull(child.findFirstToken(TokenTypes.IDENT)));
278            }
279            child = child.getNextSibling();
280        }
281        return components;
282    }
283
284}