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