001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.javadoc;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.List;
025import java.util.Set;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import com.puppycrawl.tools.checkstyle.StatelessCheck;
030import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
031import com.puppycrawl.tools.checkstyle.api.DetailAST;
032import com.puppycrawl.tools.checkstyle.api.FileContents;
033import com.puppycrawl.tools.checkstyle.api.Scope;
034import com.puppycrawl.tools.checkstyle.api.TextBlock;
035import com.puppycrawl.tools.checkstyle.api.TokenTypes;
036import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
037import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
038import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
039import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
040import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
041import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
042
043/**
044 * <div>
045 * Checks the Javadoc comments for type definitions. By default, does
046 * not check for author or version tags. The scope to verify is specified using the {@code Scope}
047 * class and defaults to {@code Scope.PRIVATE}. To verify another scope, set property
048 * scope to one of the {@code Scope} constants. To define the format for an author
049 * tag or a version tag, set property authorFormat or versionFormat respectively to a
050 * <a href="https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html">
051 * pattern</a>.
052 * </div>
053 *
054 * <p>
055 * Does not perform checks for author and version tags for inner classes,
056 * as they should be redundant because of outer class.
057 * </p>
058 *
059 * <p>
060 * Does not perform checks for type definitions that do not have any Javadoc comments.
061 * </p>
062 *
063 * <p>
064 * Error messages about type parameters and record components for which no param tags are present
065 * can be suppressed by defining property {@code allowMissingParamTags}.
066 * </p>
067 * <ul>
068 * <li>
069 * Property {@code allowMissingParamTags} - Control whether to ignore violations
070 * when a class has type parameters but does not have matching param tags in the Javadoc.
071 * Type is {@code boolean}.
072 * Default value is {@code false}.
073 * </li>
074 * <li>
075 * Property {@code allowUnknownTags} - Control whether to ignore violations when
076 * a Javadoc tag is not recognised.
077 * Type is {@code boolean}.
078 * Default value is {@code false}.
079 * </li>
080 * <li>
081 * Property {@code allowedAnnotations} - Specify annotations that allow
082 * skipping validation at all. Only short names are allowed, e.g. {@code Generated}.
083 * Type is {@code java.lang.String[]}.
084 * Default value is {@code Generated}.
085 * </li>
086 * <li>
087 * Property {@code authorFormat} - Specify the pattern for {@code @author} tag.
088 * Type is {@code java.util.regex.Pattern}.
089 * Default value is {@code null}.
090 * </li>
091 * <li>
092 * Property {@code excludeScope} - Specify the visibility scope where Javadoc
093 * comments are not checked.
094 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
095 * Default value is {@code null}.
096 * </li>
097 * <li>
098 * Property {@code scope} - Specify the visibility scope where Javadoc comments are checked.
099 * 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
154public 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}