001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.List; 025import java.util.Set; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import com.puppycrawl.tools.checkstyle.StatelessCheck; 030import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 031import com.puppycrawl.tools.checkstyle.api.DetailAST; 032import com.puppycrawl.tools.checkstyle.api.FileContents; 033import com.puppycrawl.tools.checkstyle.api.Scope; 034import com.puppycrawl.tools.checkstyle.api.TextBlock; 035import com.puppycrawl.tools.checkstyle.api.TokenTypes; 036import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 037import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 038import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 039import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 040import com.puppycrawl.tools.checkstyle.utils.ScopeUtil; 041import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 042 043/** 044 * <div> 045 * Checks the Javadoc comments for type definitions. By default, does 046 * not check for author or version tags. The scope to verify is specified using the {@code Scope} 047 * class and defaults to {@code Scope.PRIVATE}. To verify another scope, set property 048 * scope to one of the {@code Scope} constants. To define the format for an author 049 * tag or a version tag, set property authorFormat or versionFormat respectively to a 050 * <a href="https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html"> 051 * pattern</a>. 052 * </div> 053 * 054 * <p> 055 * Does not perform checks for author and version tags for inner classes, 056 * as they should be redundant because of outer class. 057 * </p> 058 * 059 * <p> 060 * Does not perform checks for type definitions that do not have any Javadoc comments. 061 * </p> 062 * 063 * <p> 064 * Error messages about type parameters and record components for which no param tags are present 065 * can be suppressed by defining property {@code allowMissingParamTags}. 066 * </p> 067 * 068 * @since 3.0 069 */ 070@StatelessCheck 071public class JavadocTypeCheck 072 extends AbstractCheck { 073 074 /** 075 * A key is pointing to the warning message text in "messages.properties" 076 * file. 077 */ 078 public static final String MSG_UNKNOWN_TAG = "javadoc.unknownTag"; 079 080 /** 081 * A key is pointing to the warning message text in "messages.properties" 082 * file. 083 */ 084 public static final String MSG_TAG_FORMAT = "type.tagFormat"; 085 086 /** 087 * A key is pointing to the warning message text in "messages.properties" 088 * file. 089 */ 090 public static final String MSG_MISSING_TAG = "type.missingTag"; 091 092 /** 093 * A key is pointing to the warning message text in "messages.properties" 094 * file. 095 */ 096 public static final String MSG_UNUSED_TAG = "javadoc.unusedTag"; 097 098 /** 099 * A key is pointing to the warning message text in "messages.properties" 100 * file. 101 */ 102 public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral"; 103 104 /** Open angle bracket literal. */ 105 private static final String OPEN_ANGLE_BRACKET = "<"; 106 107 /** Close angle bracket literal. */ 108 private static final String CLOSE_ANGLE_BRACKET = ">"; 109 110 /** Space literal. */ 111 private static final String SPACE = " "; 112 113 /** Pattern to match type name within angle brackets in javadoc param tag. */ 114 private static final Pattern TYPE_NAME_IN_JAVADOC_TAG = 115 Pattern.compile("^<([^>]+)"); 116 117 /** Pattern to split type name field in javadoc param tag. */ 118 private static final Pattern TYPE_NAME_IN_JAVADOC_TAG_SPLITTER = 119 Pattern.compile("\\s+"); 120 121 /** Specify the visibility scope where Javadoc comments are checked. */ 122 private Scope scope = Scope.PRIVATE; 123 /** Specify the visibility scope where Javadoc comments are not checked. */ 124 private Scope excludeScope; 125 /** Specify the pattern for {@code @author} tag. */ 126 private Pattern authorFormat; 127 /** Specify the pattern for {@code @version} tag. */ 128 private Pattern versionFormat; 129 /** 130 * Control whether to ignore violations when a class has type parameters but 131 * does not have matching param tags in the Javadoc. 132 */ 133 private boolean allowMissingParamTags; 134 /** Control whether to ignore violations when a Javadoc tag is not recognised. */ 135 private boolean allowUnknownTags; 136 137 /** 138 * Specify annotations that allow skipping validation at all. 139 * Only short names are allowed, e.g. {@code Generated}. 140 */ 141 private Set<String> allowedAnnotations = Set.of("Generated"); 142 143 /** 144 * Setter to specify the visibility scope where Javadoc comments are checked. 145 * 146 * @param scope a scope. 147 * @since 3.0 148 */ 149 public void setScope(Scope scope) { 150 this.scope = scope; 151 } 152 153 /** 154 * Setter to specify the visibility scope where Javadoc comments are not checked. 155 * 156 * @param excludeScope a scope. 157 * @since 3.4 158 */ 159 public void setExcludeScope(Scope excludeScope) { 160 this.excludeScope = excludeScope; 161 } 162 163 /** 164 * Setter to specify the pattern for {@code @author} tag. 165 * 166 * @param pattern a pattern. 167 * @since 3.0 168 */ 169 public void setAuthorFormat(Pattern pattern) { 170 authorFormat = pattern; 171 } 172 173 /** 174 * Setter to specify the pattern for {@code @version} tag. 175 * 176 * @param pattern a pattern. 177 * @since 3.0 178 */ 179 public void setVersionFormat(Pattern pattern) { 180 versionFormat = pattern; 181 } 182 183 /** 184 * Setter to control whether to ignore violations when a class has type parameters but 185 * does not have matching param tags in the Javadoc. 186 * 187 * @param flag a {@code Boolean} value 188 * @since 4.0 189 */ 190 public void setAllowMissingParamTags(boolean flag) { 191 allowMissingParamTags = flag; 192 } 193 194 /** 195 * Setter to control whether to ignore violations when a Javadoc tag is not recognised. 196 * 197 * @param flag a {@code Boolean} value 198 * @since 5.1 199 */ 200 public void setAllowUnknownTags(boolean flag) { 201 allowUnknownTags = flag; 202 } 203 204 /** 205 * Setter to specify annotations that allow skipping validation at all. 206 * Only short names are allowed, e.g. {@code Generated}. 207 * 208 * @param userAnnotations user's value. 209 * @since 8.15 210 */ 211 public void setAllowedAnnotations(String... userAnnotations) { 212 allowedAnnotations = Set.of(userAnnotations); 213 } 214 215 @Override 216 public int[] getDefaultTokens() { 217 return getAcceptableTokens(); 218 } 219 220 @Override 221 public int[] getAcceptableTokens() { 222 return new int[] { 223 TokenTypes.INTERFACE_DEF, 224 TokenTypes.CLASS_DEF, 225 TokenTypes.ENUM_DEF, 226 TokenTypes.ANNOTATION_DEF, 227 TokenTypes.RECORD_DEF, 228 }; 229 } 230 231 @Override 232 public int[] getRequiredTokens() { 233 return CommonUtil.EMPTY_INT_ARRAY; 234 } 235 236 // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166 237 @Override 238 @SuppressWarnings("deprecation") 239 public void visitToken(DetailAST ast) { 240 if (shouldCheck(ast)) { 241 final FileContents contents = getFileContents(); 242 final int lineNo = ast.getLineNo(); 243 final TextBlock textBlock = contents.getJavadocBefore(lineNo); 244 if (textBlock != null) { 245 final List<JavadocTag> tags = getJavadocTags(textBlock); 246 if (ScopeUtil.isOuterMostType(ast)) { 247 // don't check author/version for inner classes 248 checkTag(ast, tags, JavadocTagInfo.AUTHOR.getName(), 249 authorFormat); 250 checkTag(ast, tags, JavadocTagInfo.VERSION.getName(), 251 versionFormat); 252 } 253 254 final List<String> typeParamNames = 255 CheckUtil.getTypeParameterNames(ast); 256 final List<String> recordComponentNames = 257 getRecordComponentNames(ast); 258 259 if (!allowMissingParamTags) { 260 261 typeParamNames.forEach(typeParamName -> { 262 checkTypeParamTag(ast, tags, typeParamName); 263 }); 264 265 recordComponentNames.forEach(componentName -> { 266 checkComponentParamTag(ast, tags, componentName); 267 }); 268 } 269 270 checkUnusedParamTags(tags, typeParamNames, recordComponentNames); 271 } 272 } 273 } 274 275 /** 276 * Whether we should check this node. 277 * 278 * @param ast a given node. 279 * @return whether we should check a given node. 280 */ 281 private boolean shouldCheck(DetailAST ast) { 282 final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast); 283 284 return surroundingScope.isIn(scope) 285 && (excludeScope == null || !surroundingScope.isIn(excludeScope)) 286 && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations); 287 } 288 289 /** 290 * Gets all standalone tags from a given javadoc. 291 * 292 * @param textBlock the Javadoc comment to process. 293 * @return all standalone tags from the given javadoc. 294 */ 295 private List<JavadocTag> getJavadocTags(TextBlock textBlock) { 296 final JavadocTags tags = JavadocUtil.getJavadocTags(textBlock, 297 JavadocUtil.JavadocTagType.BLOCK); 298 if (!allowUnknownTags) { 299 tags.getInvalidTags().forEach(tag -> { 300 log(tag.getLine(), tag.getCol(), MSG_UNKNOWN_TAG, tag.getName()); 301 }); 302 } 303 return tags.getValidTags(); 304 } 305 306 /** 307 * Verifies that a type definition has a required tag. 308 * 309 * @param ast the AST node for the type definition. 310 * @param tags tags from the Javadoc comment for the type definition. 311 * @param tagName the required tag name. 312 * @param formatPattern regexp for the tag value. 313 */ 314 private void checkTag(DetailAST ast, Iterable<JavadocTag> tags, String tagName, 315 Pattern formatPattern) { 316 if (formatPattern != null) { 317 boolean hasTag = false; 318 final String tagPrefix = "@"; 319 320 for (final JavadocTag tag :tags) { 321 if (tag.getTagName().equals(tagName)) { 322 hasTag = true; 323 if (!formatPattern.matcher(tag.getFirstArg()).find()) { 324 log(ast, MSG_TAG_FORMAT, tagPrefix + tagName, formatPattern.pattern()); 325 } 326 } 327 } 328 if (!hasTag) { 329 log(ast, MSG_MISSING_TAG, tagPrefix + tagName); 330 } 331 } 332 } 333 334 /** 335 * Verifies that a record definition has the specified param tag for 336 * the specified record component name. 337 * 338 * @param ast the AST node for the record definition. 339 * @param tags tags from the Javadoc comment for the record definition. 340 * @param recordComponentName the name of the type parameter 341 */ 342 private void checkComponentParamTag(DetailAST ast, 343 Collection<JavadocTag> tags, 344 String recordComponentName) { 345 346 final boolean found = tags 347 .stream() 348 .filter(JavadocTag::isParamTag) 349 .anyMatch(tag -> tag.getFirstArg().indexOf(recordComponentName) == 0); 350 351 if (!found) { 352 log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText() 353 + SPACE + recordComponentName); 354 } 355 } 356 357 /** 358 * Verifies that a type definition has the specified param tag for 359 * the specified type parameter name. 360 * 361 * @param ast the AST node for the type definition. 362 * @param tags tags from the Javadoc comment for the type definition. 363 * @param typeParamName the name of the type parameter 364 */ 365 private void checkTypeParamTag(DetailAST ast, 366 Collection<JavadocTag> tags, String typeParamName) { 367 final String typeParamNameWithBrackets = 368 OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET; 369 370 final boolean found = tags 371 .stream() 372 .filter(JavadocTag::isParamTag) 373 .anyMatch(tag -> tag.getFirstArg().indexOf(typeParamNameWithBrackets) == 0); 374 375 if (!found) { 376 log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText() 377 + SPACE + typeParamNameWithBrackets); 378 } 379 } 380 381 /** 382 * Checks for unused param tags for type parameters and record components. 383 * 384 * @param tags tags from the Javadoc comment for the type definition 385 * @param typeParamNames names of type parameters 386 * @param recordComponentNames record component names in this definition 387 */ 388 private void checkUnusedParamTags( 389 List<JavadocTag> tags, 390 List<String> typeParamNames, 391 List<String> recordComponentNames) { 392 393 for (final JavadocTag tag: tags) { 394 if (tag.isParamTag()) { 395 final String paramName = extractParamNameFromTag(tag); 396 final boolean found = typeParamNames.contains(paramName) 397 || recordComponentNames.contains(paramName); 398 399 if (!found) { 400 if (paramName.isEmpty()) { 401 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG_GENERAL); 402 } 403 else { 404 final String actualParamName = 405 TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0]; 406 log(tag.getLineNo(), tag.getColumnNo(), 407 MSG_UNUSED_TAG, 408 JavadocTagInfo.PARAM.getText(), actualParamName); 409 } 410 } 411 } 412 } 413 414 } 415 416 /** 417 * Extracts parameter name from tag. 418 * 419 * @param tag javadoc tag to extract parameter name 420 * @return extracts type parameter name from tag 421 */ 422 private static String extractParamNameFromTag(JavadocTag tag) { 423 final String typeParamName; 424 final Matcher matchInAngleBrackets = 425 TYPE_NAME_IN_JAVADOC_TAG.matcher(tag.getFirstArg()); 426 if (matchInAngleBrackets.find()) { 427 typeParamName = matchInAngleBrackets.group(1).trim(); 428 } 429 else { 430 typeParamName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0]; 431 } 432 return typeParamName; 433 } 434 435 /** 436 * Collects the record components in a record definition. 437 * 438 * @param node the possible record definition ast. 439 * @return the record components in this record definition. 440 */ 441 private static List<String> getRecordComponentNames(DetailAST node) { 442 final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS); 443 final List<String> componentList = new ArrayList<>(); 444 445 if (components != null) { 446 TokenUtil.forEachChild(components, 447 TokenTypes.RECORD_COMPONENT_DEF, component -> { 448 final DetailAST ident = component.findFirstToken(TokenTypes.IDENT); 449 componentList.add(ident.getText()); 450 }); 451 } 452 453 return componentList; 454 } 455}