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