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/11166
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.getInvalidTags().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.getValidTags();
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)
411 .anyMatch(tag -> tag.getFirstArg().indexOf(recordComponentName) == 0);
412
413 if (!found) {
414 log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
415 + SPACE + recordComponentName);
416 }
417 }
418
419 /**
420 * Verifies that a type definition has the specified param tag for
421 * the specified type parameter name.
422 *
423 * @param ast the AST node for the type definition.
424 * @param tags tags from the Javadoc comment for the type definition.
425 * @param typeParamName the name of the type parameter
426 */
427 private void checkTypeParamTag(DetailAST ast,
428 Collection<JavadocTag> tags, String typeParamName) {
429 final String typeParamNameWithBrackets =
430 OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET;
431
432 final boolean found = tags
433 .stream()
434 .filter(JavadocTag::isParamTag)
435 .anyMatch(tag -> tag.getFirstArg().indexOf(typeParamNameWithBrackets) == 0);
436
437 if (!found) {
438 log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
439 + SPACE + typeParamNameWithBrackets);
440 }
441 }
442
443 /**
444 * Checks for unused param tags for type parameters and record components.
445 *
446 * @param tags tags from the Javadoc comment for the type definition
447 * @param typeParamNames names of type parameters
448 * @param recordComponentNames record component names in this definition
449 */
450 private void checkUnusedParamTags(
451 List<JavadocTag> tags,
452 List<String> typeParamNames,
453 List<String> recordComponentNames) {
454
455 for (final JavadocTag tag: tags) {
456 if (tag.isParamTag()) {
457 final String paramName = extractParamNameFromTag(tag);
458 final boolean found = typeParamNames.contains(paramName)
459 || recordComponentNames.contains(paramName);
460
461 if (!found) {
462 final String displayName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER
463 .split(tag.getFirstArg(), -1)[0];
464 if (displayName.isEmpty()) {
465 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG_GENERAL);
466 }
467 else {
468 log(tag.getLineNo(), tag.getColumnNo(),
469 MSG_UNUSED_TAG,
470 JavadocTagInfo.PARAM.getText(), displayName);
471 }
472 }
473 }
474 }
475
476 }
477
478 /**
479 * Extracts parameter name from tag.
480 *
481 * @param tag javadoc tag to extract parameter name
482 * @return extracts type parameter name from tag
483 */
484 private static String extractParamNameFromTag(JavadocTag tag) {
485 final String firstArg = tag.getFirstArg();
486 final Matcher matchInAngleBrackets = TYPE_NAME_IN_JAVADOC_TAG.matcher(firstArg);
487 final String paramName;
488 if (matchInAngleBrackets.find()) {
489 paramName = matchInAngleBrackets.group(1).trim();
490 }
491 else {
492 paramName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(firstArg, -1)[0];
493 }
494 return paramName;
495 }
496
497 /**
498 * Collects the record components in a record definition.
499 *
500 * @param node the possible record definition ast.
501 * @return the record components in this record definition.
502 */
503 private static List<String> getRecordComponentNames(DetailAST node) {
504 final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS);
505 final List<String> componentList = new ArrayList<>();
506
507 if (components != null) {
508 TokenUtil.forEachChild(components,
509 TokenTypes.RECORD_COMPONENT_DEF, component -> {
510 final DetailAST ident = component.findFirstToken(TokenTypes.IDENT);
511 componentList.add(ident.getText());
512 });
513 }
514
515 return componentList;
516 }
517 }