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.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.regex.Pattern;
28  
29  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.DetailNode;
32  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
33  import com.puppycrawl.tools.checkstyle.api.Scope;
34  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35  import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
36  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
39  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
40  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
41  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
42  
43  /**
44   * <div>
45   * Checks the Javadoc comments for type definitions. By default, does
46   * not check for author or version tags. The scope to verify is specified using the {@code Scope}
47   * class and defaults to {@code Scope.PRIVATE}. To verify another scope, set property
48   * scope to one of the {@code Scope} constants. To define the format for an author
49   * tag or a version tag, set property authorFormat or versionFormat respectively to a
50   * <a href="https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html">
51   * pattern</a>.
52   * </div>
53   *
54   * <p>
55   * Does not perform checks for author and version tags for inner classes,
56   * as they should be redundant because of outer class.
57   * </p>
58   *
59   * <p>
60   * Does not perform checks for type definitions that do not have any Javadoc comments.
61   * </p>
62   *
63   * <p>
64   * Error messages about type parameters and record components for which no param tags are present
65   * can be suppressed by defining property {@code allowMissingParamTags}.
66   * </p>
67   *
68   * @since 3.0
69   */
70  @FileStatefulCheck
71  public class JavadocTypeCheck extends AbstractJavadocCheck {
72  
73      /**
74       * A key is pointing to the warning message text in "messages.properties"
75       * file.
76       */
77      public static final String MSG_UNKNOWN_TAG = "javadoc.unknownTag";
78  
79      /**
80       * A key is pointing to the warning message text in "messages.properties"
81       * file.
82       */
83      public static final String MSG_TAG_FORMAT = "type.tagFormat";
84  
85      /**
86       * A key is pointing to the warning message text in "messages.properties"
87       * file.
88       */
89      public static final String MSG_MISSING_TAG = "type.missingTag";
90  
91      /**
92       * A key is pointing to the warning message text in "messages.properties"
93       * file.
94       */
95      public static final String MSG_MISSING_TAG_WITH_QUOTES =
96              "type.missingTagWithQuotes";
97  
98      /**
99       * A key is pointing to the warning message text in "messages.properties"
100      * file.
101      */
102     public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
103 
104     /**
105      * A key is pointing to the warning message text in "messages.properties"
106      * file.
107      */
108     public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
109 
110     /** Open angle bracket literal. */
111     private static final String OPEN_ANGLE_BRACKET = "<";
112 
113     /** Close angle bracket literal. */
114     private static final String CLOSE_ANGLE_BRACKET = ">";
115 
116     /** Author tag literal. */
117     private static final String AUTHOR_TAG_NAME = "@author";
118 
119     /** Version tag literal. */
120     private static final String VERSION_TAG_NAME = "@version";
121 
122     /** Javadoc param tag names, mapped by their corresponding Javadoc node. */
123     private final Map<DetailNode, String> javadocTags = new LinkedHashMap<>();
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     /** Java AST node whose attached Javadoc is currently being processed. */
148     private DetailAST currentAst;
149 
150     /** Whether an {@code @author} tag was found in the current Javadoc tree. */
151     private boolean authorTagIsPresent;
152 
153     /** Whether a {@code @version} tag was found in the current Javadoc tree. */
154     private boolean versionTagIsPresent;
155 
156     /**
157      * Creates a new {@code JavadocTypeCheck} instance.
158      */
159     public JavadocTypeCheck() {
160         // no code by default
161     }
162 
163     /**
164      * Setter to specify the visibility scope where Javadoc comments are checked.
165      *
166      * @param scope a scope.
167      * @since 3.0
168      */
169     public void setScope(Scope scope) {
170         this.scope = scope;
171     }
172 
173     /**
174      * Setter to specify the visibility scope where Javadoc comments are not checked.
175      *
176      * @param excludeScope a scope.
177      * @since 3.4
178      */
179     public void setExcludeScope(Scope excludeScope) {
180         this.excludeScope = excludeScope;
181     }
182 
183     /**
184      * Setter to specify the pattern for {@code @author} tag.
185      *
186      * @param pattern a pattern.
187      * @since 3.0
188      */
189     public void setAuthorFormat(Pattern pattern) {
190         authorFormat = pattern;
191     }
192 
193     /**
194      * Setter to specify the pattern for {@code @version} tag.
195      *
196      * @param pattern a pattern.
197      * @since 3.0
198      */
199     public void setVersionFormat(Pattern pattern) {
200         versionFormat = pattern;
201     }
202 
203     /**
204      * Setter to control whether to ignore violations when a class has type parameters but
205      * does not have matching param tags in the Javadoc.
206      *
207      * @param flag a {@code Boolean} value
208      * @since 4.0
209      */
210     public void setAllowMissingParamTags(boolean flag) {
211         allowMissingParamTags = flag;
212     }
213 
214     /**
215      * Setter to control whether to ignore violations when a Javadoc tag is not recognised.
216      *
217      * @param flag a {@code Boolean} value
218      * @since 5.1
219      */
220     public void setAllowUnknownTags(boolean flag) {
221         allowUnknownTags = flag;
222     }
223 
224     /**
225      * Setter to specify annotations that allow skipping validation at all.
226      * Only short names are allowed, e.g. {@code Generated}.
227      *
228      * @param userAnnotations user's value.
229      * @since 8.15
230      */
231     public void setAllowedAnnotations(String... userAnnotations) {
232         allowedAnnotations = Set.of(userAnnotations);
233     }
234 
235     /**
236      * Setter to control when to print violations if the Javadoc being examined by this check
237      * violates the tight html rules defined at
238      * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
239      *     Tight-HTML Rules</a>.
240      *
241      * @param shouldReportViolation value to which the field shall be set to
242      * @since 8.3
243      * @propertySince 13.9.0
244      */
245     @Override
246     public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
247         super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
248     }
249 
250     @Override
251     public void beginJavadocTree(DetailNode rootAst) {
252         javadocTags.clear();
253         authorTagIsPresent = false;
254         versionTagIsPresent = false;
255     }
256 
257     @Override
258     public void finishJavadocTree(DetailNode rootAst) {
259         if (authorFormat != null && !authorTagIsPresent
260                 && ScopeUtil.isOuterMostType(currentAst)) {
261             log(currentAst, MSG_MISSING_TAG, AUTHOR_TAG_NAME);
262         }
263         if (versionFormat != null && !versionTagIsPresent
264                 && ScopeUtil.isOuterMostType(currentAst)) {
265             log(currentAst, MSG_MISSING_TAG, VERSION_TAG_NAME);
266         }
267         checkCollectedParamTags();
268     }
269 
270     @Override
271     public int[] getDefaultJavadocTokens() {
272         return getRequiredJavadocTokens();
273     }
274 
275     @Override
276     public int[] getRequiredJavadocTokens() {
277         return new int[] {
278             JavadocCommentsTokenTypes.PARAM_BLOCK_TAG,
279             JavadocCommentsTokenTypes.AUTHOR_BLOCK_TAG,
280             JavadocCommentsTokenTypes.VERSION_BLOCK_TAG,
281             JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG,
282         };
283     }
284 
285     @Override
286     public void visitJavadocToken(DetailNode ast) {
287         switch (ast.getType()) {
288             case JavadocCommentsTokenTypes.PARAM_BLOCK_TAG -> collectParam(ast);
289             case JavadocCommentsTokenTypes.AUTHOR_BLOCK_TAG -> {
290                 authorTagIsPresent = true;
291                 checkTagFormat(ast, AUTHOR_TAG_NAME, authorFormat);
292             }
293             case JavadocCommentsTokenTypes.VERSION_BLOCK_TAG -> {
294                 versionTagIsPresent = true;
295                 checkTagFormat(ast, VERSION_TAG_NAME, versionFormat);
296             }
297             case JavadocCommentsTokenTypes.CUSTOM_BLOCK_TAG -> checkUnknownTag(ast);
298             default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
299         }
300     }
301 
302     @Override
303     public int[] getDefaultTokens() {
304         return getAcceptableTokens();
305     }
306 
307     @Override
308     public int[] getAcceptableTokens() {
309         return new int[] {
310             TokenTypes.INTERFACE_DEF,
311             TokenTypes.CLASS_DEF,
312             TokenTypes.ENUM_DEF,
313             TokenTypes.ANNOTATION_DEF,
314             TokenTypes.RECORD_DEF,
315         };
316     }
317 
318     @Override
319     public int[] getRequiredTokens() {
320         return CommonUtil.EMPTY_INT_ARRAY;
321     }
322 
323     @Override
324     public void visitToken(DetailAST ast) {
325         if (shouldCheck(ast)) {
326             final DetailAST blockCommentNode = JavadocUtil.getAttachedJavadocComment(ast);
327             if (blockCommentNode != null) {
328                 currentAst = ast;
329                 super.visitToken(blockCommentNode);
330             }
331         }
332     }
333 
334     /**
335      * Whether we should check this node.
336      *
337      * @param ast a given node.
338      * @return whether we should check a given node.
339      */
340     private boolean shouldCheck(DetailAST ast) {
341         return ScopeUtil.getSurroundingScope(ast)
342             .map(surroundingScope -> {
343                 return surroundingScope.isIn(scope)
344                     && (excludeScope == null || !surroundingScope.isIn(excludeScope))
345                     && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations);
346             })
347             .orElse(Boolean.FALSE);
348     }
349 
350     /**
351      * Collects a param tag.
352      *
353      * @param ast the param tag node
354      */
355     private void collectParam(DetailNode ast) {
356         final DetailNode parameterName = JavadocUtil.findFirstToken(
357                 ast, JavadocCommentsTokenTypes.PARAMETER_NAME);
358         if (parameterName != null) {
359             javadocTags.put(ast, parameterName.getText());
360         }
361         else {
362             log(ast, MSG_UNUSED_TAG_GENERAL);
363         }
364     }
365 
366     /**
367      * Checks an unknown Javadoc tag.
368      *
369      * @param ast the unknown tag node
370      */
371     private void checkUnknownTag(DetailNode ast) {
372         if (!allowUnknownTags) {
373             final String tagName = JavadocUtil.findFirstToken(
374                     ast, JavadocCommentsTokenTypes.TAG_NAME).getText();
375             log(ast, MSG_UNKNOWN_TAG, tagName);
376         }
377     }
378 
379     /**
380      * Checks a Javadoc tag description against the expected format.
381      *
382      * @param ast the Javadoc tag node
383      * @param tagName the tag name
384      * @param format expected format for the tag description
385      */
386     private void checkTagFormat(DetailNode ast, String tagName, Pattern format) {
387         if (format != null && ScopeUtil.isOuterMostType(currentAst)) {
388             String description = "";
389             final DetailNode descriptionNode = JavadocUtil.findFirstToken(
390                     ast, JavadocCommentsTokenTypes.DESCRIPTION);
391             if (descriptionNode != null) {
392                 description = descriptionNode.getFirstChild().getText().trim();
393             }
394             if (!format.matcher(description).find()) {
395                 log(currentAst, MSG_TAG_FORMAT, tagName, format.pattern());
396             }
397         }
398     }
399 
400     /**
401      * Checks collected Javadoc param tags against the current AST node.
402      */
403     private void checkCollectedParamTags() {
404         final List<String> params = getRecordComponentNames(currentAst);
405         final List<String> typeParamNames = CheckUtil.getTypeParameterNames(currentAst);
406 
407         for (Map.Entry<DetailNode, String> tag : javadocTags.entrySet()) {
408             final String paramName = tag.getValue();
409             boolean found = params.remove(paramName);
410             if (paramName.startsWith(OPEN_ANGLE_BRACKET)) {
411                 final String typeParamName = paramName.substring(1, paramName.length() - 1);
412                 found = typeParamNames.remove(typeParamName);
413             }
414 
415             if (!found) {
416                 log(tag.getKey(), MSG_UNUSED_TAG, JavadocTagInfo.PARAM.getText(), paramName);
417             }
418         }
419 
420         if (!allowMissingParamTags) {
421             params.forEach(paramName -> {
422                 log(currentAst, MSG_MISSING_TAG_WITH_QUOTES,
423                     JavadocTagInfo.PARAM.getText(), paramName);
424             });
425             typeParamNames.forEach(typeParamName -> {
426                 log(currentAst, MSG_MISSING_TAG_WITH_QUOTES,
427                     JavadocTagInfo.PARAM.getText(),
428                     OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET);
429             });
430         }
431     }
432 
433     /**
434      * Collects the record component names in a record definition.
435      *
436      * @param node the possible record definition AST
437      * @return the record component names in this record definition
438      */
439     private static List<String> getRecordComponentNames(DetailAST node) {
440         final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS);
441         final List<String> componentNames = new ArrayList<>();
442 
443         if (components != null) {
444             TokenUtil.forEachChild(components,
445                 TokenTypes.RECORD_COMPONENT_DEF, component -> {
446                     final DetailAST ident =
447                             NullUtil.notNull(component.findFirstToken(TokenTypes.IDENT));
448                     componentNames.add(ident.getText());
449                 });
450         }
451 
452         return componentNames;
453     }
454 
455 }