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