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.Arrays;
24 import java.util.Collection;
25 import java.util.List;
26 import java.util.Set;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29
30 import com.puppycrawl.tools.checkstyle.StatelessCheck;
31 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
32 import com.puppycrawl.tools.checkstyle.api.DetailAST;
33 import com.puppycrawl.tools.checkstyle.api.FileContents;
34 import com.puppycrawl.tools.checkstyle.api.Scope;
35 import com.puppycrawl.tools.checkstyle.api.TextBlock;
36 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
38 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
39 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
40 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
41 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
42 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
43
44 /**
45 * <div>
46 * Checks the Javadoc comments for type definitions. By default, does
47 * not check for author or version tags. The scope to verify is specified using the {@code Scope}
48 * class and defaults to {@code Scope.PRIVATE}. To verify another scope, set property
49 * scope to one of the {@code Scope} constants. To define the format for an author
50 * tag or a version tag, set property authorFormat or versionFormat respectively to a
51 * <a href="https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html">
52 * pattern</a>.
53 * </div>
54 *
55 * <p>
56 * Does not perform checks for author and version tags for inner classes,
57 * as they should be redundant because of outer class.
58 * </p>
59 *
60 * <p>
61 * Does not perform checks for type definitions that do not have any Javadoc comments.
62 * </p>
63 *
64 * <p>
65 * Error messages about type parameters and record components for which no param tags are present
66 * can be suppressed by defining property {@code allowMissingParamTags}.
67 * </p>
68 *
69 * @since 3.0
70 */
71 @StatelessCheck
72 public class JavadocTypeCheck
73 extends AbstractCheck {
74
75 /**
76 * A key is pointing to the warning message text in "messages.properties"
77 * file.
78 */
79 public static final String MSG_UNKNOWN_TAG = "javadoc.unknownTag";
80
81 /**
82 * A key is pointing to the warning message text in "messages.properties"
83 * file.
84 */
85 public static final String MSG_TAG_FORMAT = "type.tagFormat";
86
87 /**
88 * A key is pointing to the warning message text in "messages.properties"
89 * file.
90 */
91 public static final String MSG_MISSING_TAG = "type.missingTag";
92
93 /**
94 * A key is pointing to the warning message text in "messages.properties"
95 * file.
96 */
97 public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
98
99 /**
100 * A key is pointing to the warning message text in "messages.properties"
101 * file.
102 */
103 public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
104
105 /** Open angle bracket literal. */
106 private static final String OPEN_ANGLE_BRACKET = "<";
107
108 /** Close angle bracket literal. */
109 private static final String CLOSE_ANGLE_BRACKET = ">";
110
111 /** Space literal. */
112 private static final String SPACE = " ";
113
114 /** Javadoc tag token literal. */
115 private static final String JAVADOC_TAG_TOKEN = "@";
116
117 /** Pattern to match type name within angle brackets in javadoc param tag. */
118 private static final Pattern TYPE_NAME_IN_JAVADOC_TAG =
119 Pattern.compile("^<([^>]+)");
120
121 /** Pattern to split type name field in javadoc param tag. */
122 private static final Pattern TYPE_NAME_IN_JAVADOC_TAG_SPLITTER =
123 Pattern.compile("\\s+");
124
125 /** Specify the visibility scope where Javadoc comments are checked. */
126 private Scope scope = Scope.PRIVATE;
127 /** Specify the visibility scope where Javadoc comments are not checked. */
128 private Scope excludeScope;
129 /** Specify the pattern for {@code @author} tag. */
130 private Pattern authorFormat;
131 /** Specify the pattern for {@code @version} tag. */
132 private Pattern versionFormat;
133 /**
134 * Control whether to ignore violations when a class has type parameters but
135 * does not have matching param tags in the Javadoc.
136 */
137 private boolean allowMissingParamTags;
138 /** Control whether to ignore violations when a Javadoc tag is not recognised. */
139 private boolean allowUnknownTags;
140
141 /**
142 * Specify annotations that allow skipping validation at all.
143 * Only short names are allowed, e.g. {@code Generated}.
144 */
145 private Set<String> allowedAnnotations = Set.of("Generated");
146
147 /**
148 * Setter to specify the visibility scope where Javadoc comments are checked.
149 *
150 * @param scope a scope.
151 * @since 3.0
152 */
153 public void setScope(Scope scope) {
154 this.scope = scope;
155 }
156
157 /**
158 * Setter to specify the visibility scope where Javadoc comments are not checked.
159 *
160 * @param excludeScope a scope.
161 * @since 3.4
162 */
163 public void setExcludeScope(Scope excludeScope) {
164 this.excludeScope = excludeScope;
165 }
166
167 /**
168 * Setter to specify the pattern for {@code @author} tag.
169 *
170 * @param pattern a pattern.
171 * @since 3.0
172 */
173 public void setAuthorFormat(Pattern pattern) {
174 authorFormat = pattern;
175 }
176
177 /**
178 * Setter to specify the pattern for {@code @version} tag.
179 *
180 * @param pattern a pattern.
181 * @since 3.0
182 */
183 public void setVersionFormat(Pattern pattern) {
184 versionFormat = pattern;
185 }
186
187 /**
188 * Setter to control whether to ignore violations when a class has type parameters but
189 * does not have matching param tags in the Javadoc.
190 *
191 * @param flag a {@code Boolean} value
192 * @since 4.0
193 */
194 public void setAllowMissingParamTags(boolean flag) {
195 allowMissingParamTags = flag;
196 }
197
198 /**
199 * Setter to control whether to ignore violations when a Javadoc tag is not recognised.
200 *
201 * @param flag a {@code Boolean} value
202 * @since 5.1
203 */
204 public void setAllowUnknownTags(boolean flag) {
205 allowUnknownTags = flag;
206 }
207
208 /**
209 * Setter to specify annotations that allow skipping validation at all.
210 * Only short names are allowed, e.g. {@code Generated}.
211 *
212 * @param userAnnotations user's value.
213 * @since 8.15
214 */
215 public void setAllowedAnnotations(String... userAnnotations) {
216 allowedAnnotations = Set.of(userAnnotations);
217 }
218
219 @Override
220 public int[] getDefaultTokens() {
221 return getAcceptableTokens();
222 }
223
224 @Override
225 public int[] getAcceptableTokens() {
226 return new int[] {
227 TokenTypes.INTERFACE_DEF,
228 TokenTypes.CLASS_DEF,
229 TokenTypes.ENUM_DEF,
230 TokenTypes.ANNOTATION_DEF,
231 TokenTypes.RECORD_DEF,
232 };
233 }
234
235 @Override
236 public int[] getRequiredTokens() {
237 return CommonUtil.EMPTY_INT_ARRAY;
238 }
239
240 // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/19146
241 @Override
242 @SuppressWarnings("deprecation")
243 public void visitToken(DetailAST ast) {
244 if (shouldCheck(ast)) {
245 final FileContents contents = getFileContents();
246 final int lineNo = ast.getLineNo();
247 final TextBlock textBlock = contents.getJavadocBefore(lineNo);
248 if (textBlock != null) {
249 final List<JavadocTag> tags = getJavadocTags(textBlock);
250 if (ScopeUtil.isOuterMostType(ast)) {
251 // don't check author/version for inner classes
252 checkTag(ast, tags, JavadocTagInfo.AUTHOR.getName(),
253 authorFormat);
254 checkTag(ast, tags, JavadocTagInfo.VERSION.getName(),
255 versionFormat);
256 }
257
258 final List<String> typeParamNames =
259 CheckUtil.getTypeParameterNames(ast);
260 final List<String> recordComponentNames =
261 getRecordComponentNames(ast);
262
263 if (!allowMissingParamTags) {
264
265 typeParamNames.forEach(typeParamName -> {
266 checkTypeParamTag(ast, tags, typeParamName);
267 });
268
269 recordComponentNames.forEach(componentName -> {
270 checkComponentParamTag(ast, tags, componentName);
271 });
272 }
273
274 checkUnusedParamTags(tags, typeParamNames, recordComponentNames);
275 }
276 }
277 }
278
279 /**
280 * Whether we should check this node.
281 *
282 * @param ast a given node.
283 * @return whether we should check a given node.
284 */
285 private boolean shouldCheck(DetailAST ast) {
286 return ScopeUtil.getSurroundingScope(ast)
287 .map(surroundingScope -> {
288 return surroundingScope.isIn(scope)
289 && (excludeScope == null || !surroundingScope.isIn(excludeScope))
290 && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations);
291 })
292 .orElse(Boolean.FALSE);
293 }
294
295 /**
296 * Gets all standalone tags from a given javadoc.
297 *
298 * @param textBlock the Javadoc comment to process.
299 * @return all standalone tags from the given javadoc.
300 */
301 private List<JavadocTag> getJavadocTags(TextBlock textBlock) {
302 final JavadocTags tags = JavadocUtil.getJavadocTags(textBlock,
303 JavadocUtil.JavadocTagType.BLOCK);
304 if (!allowUnknownTags) {
305 final String[] lines = textBlock.getText();
306 tags.invalidTags().stream()
307 .filter(tag -> !isTagInsideCodeOrLiteralBlock(lines, textBlock, tag))
308 .forEach(tag -> {
309 log(tag.getLine(), tag.getCol(), MSG_UNKNOWN_TAG, tag.getName());
310 });
311 }
312 return tags.validTags();
313 }
314
315 /**
316 * Checks if a tag is positioned inside a {@code @code} or {@code @literal} inline tag block.
317 * Since block tags must appear at line-start position (per BlockTagUtil regex pattern),
318 * we only need to check content from previous lines - there cannot be inline content
319 * before a block tag on the same line.
320 *
321 * @param lines the Javadoc comment lines.
322 * @param textBlock the text block containing the Javadoc.
323 * @param tag the invalid tag to check.
324 * @return true if the tag is inside a code or literal block.
325 */
326 private static boolean isTagInsideCodeOrLiteralBlock(String[] lines,
327 TextBlock textBlock,
328 InvalidJavadocTag tag) {
329 final int tagLineIndex = tag.getLine() - textBlock.getStartLineNo();
330
331 final String textBefore = String.join("\n", Arrays.copyOfRange(lines, 0, tagLineIndex));
332 return isInsideInlineTag(textBefore);
333 }
334
335 /**
336 * Determines if the position is inside an unclosed {@code @code}, {@code @literal},
337 * or {@code @snippet} inline tag by counting opening and closing braces.
338 * These tags display content verbatim and should not be parsed for Javadoc block tags.
339 *
340 * @param textBefore the text from the start of Javadoc up to the tag position.
341 * @return true if inside an unclosed code, literal, or snippet inline tag.
342 */
343 private static boolean isInsideInlineTag(String textBefore) {
344 boolean insideVerbatimTag = false;
345 int braceDepth = 0;
346
347 for (int index = 0; index < textBefore.length(); index++) {
348 final char ch = textBefore.charAt(index);
349 if (ch == '{') {
350 if (textBefore.startsWith("{@code", index)
351 || textBefore.startsWith("{@literal", index)
352 || textBefore.startsWith("{@snippet", index)) {
353 insideVerbatimTag = true;
354 }
355 braceDepth++;
356 }
357 else if (ch == '}') {
358 braceDepth--;
359 if (braceDepth == 0) {
360 insideVerbatimTag = false;
361 }
362 }
363 }
364
365 return insideVerbatimTag;
366 }
367
368 /**
369 * Verifies that a type definition has a required tag.
370 *
371 * @param ast the AST node for the type definition.
372 * @param tags tags from the Javadoc comment for the type definition.
373 * @param tagName the required tag name.
374 * @param formatPattern regexp for the tag value.
375 */
376 private void checkTag(DetailAST ast, Iterable<JavadocTag> tags, String tagName,
377 Pattern formatPattern) {
378 if (formatPattern != null) {
379 boolean hasTag = false;
380
381 for (final JavadocTag tag : tags) {
382 if (tag.getTagName().equals(tagName)) {
383 hasTag = true;
384 if (!formatPattern.matcher(tag.getFirstArg()).find()) {
385 log(ast, MSG_TAG_FORMAT, JAVADOC_TAG_TOKEN + tagName,
386 formatPattern.pattern());
387 }
388 }
389 }
390 if (!hasTag) {
391 log(ast, MSG_MISSING_TAG, JAVADOC_TAG_TOKEN + tagName);
392 }
393 }
394 }
395
396 /**
397 * Verifies that a record definition has the specified param tag for
398 * the specified record component name.
399 *
400 * @param ast the AST node for the record definition.
401 * @param tags tags from the Javadoc comment for the record definition.
402 * @param recordComponentName the name of the type parameter
403 */
404 private void checkComponentParamTag(DetailAST ast,
405 Collection<JavadocTag> tags,
406 String recordComponentName) {
407
408 final boolean found = tags
409 .stream()
410 .filter(JavadocTag::isParamTag).anyMatch(tag -> {
411 final String arg = tag.getFirstArg();
412 return arg.equals(recordComponentName)
413 || arg.startsWith(recordComponentName + SPACE);
414 });
415
416 if (!found) {
417 log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
418 + SPACE + recordComponentName);
419 }
420 }
421
422 /**
423 * Verifies that a type definition has the specified param tag for
424 * the specified type parameter name.
425 *
426 * @param ast the AST node for the type definition.
427 * @param tags tags from the Javadoc comment for the type definition.
428 * @param typeParamName the name of the type parameter
429 */
430 private void checkTypeParamTag(DetailAST ast,
431 Collection<JavadocTag> tags, String typeParamName) {
432 final String typeParamNameWithBrackets =
433 OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET;
434
435 final boolean found = tags
436 .stream()
437 .filter(JavadocTag::isParamTag)
438 .anyMatch(tag -> tag.getFirstArg().indexOf(typeParamNameWithBrackets) == 0);
439
440 if (!found) {
441 log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
442 + SPACE + typeParamNameWithBrackets);
443 }
444 }
445
446 /**
447 * Checks for unused param tags for type parameters and record components.
448 *
449 * @param tags tags from the Javadoc comment for the type definition
450 * @param typeParamNames names of type parameters
451 * @param recordComponentNames record component names in this definition
452 */
453 private void checkUnusedParamTags(
454 List<JavadocTag> tags,
455 List<String> typeParamNames,
456 List<String> recordComponentNames) {
457
458 for (final JavadocTag tag: tags) {
459 if (tag.isParamTag()) {
460 final String paramName = extractParamNameFromTag(tag);
461 final boolean found = typeParamNames.contains(paramName)
462 || recordComponentNames.contains(paramName);
463
464 if (!found) {
465 final String displayName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER
466 .split(tag.getFirstArg(), -1)[0];
467 if (displayName.isEmpty()) {
468 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG_GENERAL);
469 }
470 else {
471 log(tag.getLineNo(), tag.getColumnNo(),
472 MSG_UNUSED_TAG,
473 JavadocTagInfo.PARAM.getText(), displayName);
474 }
475 }
476 }
477 }
478
479 }
480
481 /**
482 * Extracts parameter name from tag.
483 *
484 * @param tag javadoc tag to extract parameter name
485 * @return extracts type parameter name from tag
486 */
487 private static String extractParamNameFromTag(JavadocTag tag) {
488 final String firstArg = tag.getFirstArg();
489 final Matcher matchInAngleBrackets = TYPE_NAME_IN_JAVADOC_TAG.matcher(firstArg);
490 final String paramName;
491 if (matchInAngleBrackets.find()) {
492 paramName = matchInAngleBrackets.group(1).trim();
493 }
494 else {
495 paramName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(firstArg, -1)[0];
496 }
497 return paramName;
498 }
499
500 /**
501 * Collects the record components in a record definition.
502 *
503 * @param node the possible record definition ast.
504 * @return the record components in this record definition.
505 */
506 private static List<String> getRecordComponentNames(DetailAST node) {
507 final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS);
508 final List<String> componentList = new ArrayList<>();
509
510 if (components != null) {
511 TokenUtil.forEachChild(components,
512 TokenTypes.RECORD_COMPONENT_DEF, component -> {
513 final DetailAST ident = component.findFirstToken(TokenTypes.IDENT);
514 componentList.add(ident.getText());
515 });
516 }
517
518 return componentList;
519 }
520 }