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.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 }