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.javadoc;
21  
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.LinkedHashMap;
25  import java.util.List;
26  import java.util.Map;
27  
28  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29  import com.puppycrawl.tools.checkstyle.api.DetailAST;
30  import com.puppycrawl.tools.checkstyle.api.DetailNode;
31  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
34  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
35  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
36  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
37  
38  /**
39   * <div>
40   * Checks that {@code @param} tags in Javadoc comments are in the same order as the
41   * parameters in the declaration.
42   * </div>
43   *
44   * <p>
45   * Type parameters must come before regular parameters. For record declarations, record
46   * components are treated as regular parameters and must be documented after type parameters.
47   * For compact constructors, the expected parameter order is the order of the record components
48   * in the record declaration.
49   * </p>
50   *
51   * <p>
52   * The check does not validate missing, extra, or duplicate {@code @param} tags. It reports only
53   * tags that move backward in the declaration order.
54   * </p>
55   *
56   * @since 14.1.0
57   */
58  @FileStatefulCheck
59  public class JavadocParamOrderCheck extends AbstractJavadocCheck {
60  
61      /**
62       * A key is pointing to the warning message text in "messages.properties"
63       * file.
64       */
65      public static final String MSG_KEY = "javadoc.param.order";
66  
67      /** Html element start symbol. */
68      private static final String ELEMENT_START = "<";
69  
70      /** Html element end symbol. */
71      private static final String ELEMENT_END = ">";
72  
73      /** Javadoc param tag names, mapped by their corresponding Javadoc node. */
74      private final Map<DetailNode, String> javadocTags = new LinkedHashMap<>();
75  
76      /**
77       * Creates a new {@code JavadocParamOrderCheck} instance.
78       */
79      public JavadocParamOrderCheck() {
80          // no code by default
81      }
82  
83      @Override
84      public int[] getDefaultTokens() {
85          return getRequiredTokens();
86      }
87  
88      @Override
89      public final int[] getRequiredTokens() {
90          return new int[] {
91              TokenTypes.METHOD_DEF,
92              TokenTypes.CTOR_DEF,
93              TokenTypes.CLASS_DEF,
94              TokenTypes.INTERFACE_DEF,
95              TokenTypes.COMPACT_CTOR_DEF,
96              TokenTypes.RECORD_DEF,
97          };
98      }
99  
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 }