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}