001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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 * <p>
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 * </p>
053 * <p>
054 * Does not perform checks for author and version tags for inner classes,
055 * as they should be redundant because of outer class.
056 * </p>
057 * <p>
058 * Does not perform checks for type definitions that do not have any Javadoc comments.
059 * </p>
060 * <p>
061 * Error messages about type parameters and record components for which no param tags are present
062 * can be suppressed by defining property {@code allowMissingParamTags}.
063 * </p>
064 * <ul>
065 * <li>
066 * Property {@code allowMissingParamTags} - Control whether to ignore violations
067 * when a class has type parameters but does not have matching param tags in the Javadoc.
068 * Type is {@code boolean}.
069 * Default value is {@code false}.
070 * </li>
071 * <li>
072 * Property {@code allowUnknownTags} - Control whether to ignore violations when
073 * a Javadoc tag is not recognised.
074 * Type is {@code boolean}.
075 * Default value is {@code false}.
076 * </li>
077 * <li>
078 * Property {@code allowedAnnotations} - Specify annotations that allow
079 * skipping validation at all. Only short names are allowed, e.g. {@code Generated}.
080 * Type is {@code java.lang.String[]}.
081 * Default value is {@code Generated}.
082 * </li>
083 * <li>
084 * Property {@code authorFormat} - Specify the pattern for {@code @author} tag.
085 * Type is {@code java.util.regex.Pattern}.
086 * Default value is {@code null}.
087 * </li>
088 * <li>
089 * Property {@code excludeScope} - Specify the visibility scope where Javadoc
090 * comments are not checked.
091 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
092 * Default value is {@code null}.
093 * </li>
094 * <li>
095 * Property {@code scope} - Specify the visibility scope where Javadoc comments are checked.
096 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
097 * Default value is {@code private}.
098 * </li>
099 * <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
149public 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("\\s*<([^>]+)>.*");
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 customScope = ScopeUtil.getScope(ast);
361        final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
362
363        return customScope.isIn(scope)
364            && (surroundingScope == null || surroundingScope.isIn(scope))
365            && (excludeScope == null
366                || !customScope.isIn(excludeScope)
367                || surroundingScope != null
368                && !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            for (final InvalidJavadocTag tag : tags.getInvalidTags()) {
383                log(tag.getLine(), tag.getCol(), MSG_UNKNOWN_TAG,
384                    tag.getName());
385            }
386        }
387        return tags.getValidTags();
388    }
389
390    /**
391     * Verifies that a type definition has a required tag.
392     *
393     * @param ast the AST node for the type definition.
394     * @param tags tags from the Javadoc comment for the type definition.
395     * @param tagName the required tag name.
396     * @param formatPattern regexp for the tag value.
397     */
398    private void checkTag(DetailAST ast, Iterable<JavadocTag> tags, String tagName,
399                          Pattern formatPattern) {
400        if (formatPattern != null) {
401            boolean hasTag = false;
402            final String tagPrefix = "@";
403
404            for (final JavadocTag tag :tags) {
405                if (tag.getTagName().equals(tagName)) {
406                    hasTag = true;
407                    if (!formatPattern.matcher(tag.getFirstArg()).find()) {
408                        log(ast, MSG_TAG_FORMAT, tagPrefix + tagName, formatPattern.pattern());
409                    }
410                }
411            }
412            if (!hasTag) {
413                log(ast, MSG_MISSING_TAG, tagPrefix + tagName);
414            }
415        }
416    }
417
418    /**
419     * Verifies that a record definition has the specified param tag for
420     * the specified record component name.
421     *
422     * @param ast the AST node for the record definition.
423     * @param tags tags from the Javadoc comment for the record definition.
424     * @param recordComponentName the name of the type parameter
425     */
426    private void checkComponentParamTag(DetailAST ast,
427                                        Collection<JavadocTag> tags,
428                                        String recordComponentName) {
429
430        final boolean found = tags
431            .stream()
432            .filter(JavadocTag::isParamTag)
433            .anyMatch(tag -> tag.getFirstArg().indexOf(recordComponentName) == 0);
434
435        if (!found) {
436            log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
437                + SPACE + recordComponentName);
438        }
439    }
440
441    /**
442     * Verifies that a type definition has the specified param tag for
443     * the specified type parameter name.
444     *
445     * @param ast the AST node for the type definition.
446     * @param tags tags from the Javadoc comment for the type definition.
447     * @param typeParamName the name of the type parameter
448     */
449    private void checkTypeParamTag(DetailAST ast,
450            Collection<JavadocTag> tags, String typeParamName) {
451        final String typeParamNameWithBrackets =
452            OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET;
453
454        final boolean found = tags
455            .stream()
456            .filter(JavadocTag::isParamTag)
457            .anyMatch(tag -> tag.getFirstArg().indexOf(typeParamNameWithBrackets) == 0);
458
459        if (!found) {
460            log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
461                + SPACE + typeParamNameWithBrackets);
462        }
463    }
464
465    /**
466     * Checks for unused param tags for type parameters and record components.
467     *
468     * @param tags tags from the Javadoc comment for the type definition
469     * @param typeParamNames names of type parameters
470     * @param recordComponentNames record component names in this definition
471     */
472    private void checkUnusedParamTags(
473        List<JavadocTag> tags,
474        List<String> typeParamNames,
475        List<String> recordComponentNames) {
476
477        for (final JavadocTag tag: tags) {
478            if (tag.isParamTag()) {
479                final String paramName = extractParamNameFromTag(tag);
480                final boolean found = typeParamNames.contains(paramName)
481                        || recordComponentNames.contains(paramName);
482
483                if (!found) {
484                    final String actualParamName =
485                        TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0];
486                    log(tag.getLineNo(), tag.getColumnNo(),
487                        MSG_UNUSED_TAG,
488                        JavadocTagInfo.PARAM.getText(), actualParamName);
489                }
490            }
491        }
492
493    }
494
495    /**
496     * Extracts parameter name from tag.
497     *
498     * @param tag javadoc tag to extract parameter name
499     * @return extracts type parameter name from tag
500     */
501    private static String extractParamNameFromTag(JavadocTag tag) {
502        final String typeParamName;
503        final Matcher matchInAngleBrackets =
504                TYPE_NAME_IN_JAVADOC_TAG.matcher(tag.getFirstArg());
505        if (matchInAngleBrackets.find()) {
506            typeParamName = matchInAngleBrackets.group(1).trim();
507        }
508        else {
509            typeParamName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0];
510        }
511        return typeParamName;
512    }
513
514    /**
515     * Collects the record components in a record definition.
516     *
517     * @param node the possible record definition ast.
518     * @return the record components in this record definition.
519     */
520    private static List<String> getRecordComponentNames(DetailAST node) {
521        final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS);
522        final List<String> componentList = new ArrayList<>();
523
524        if (components != null) {
525            TokenUtil.forEachChild(components,
526                TokenTypes.RECORD_COMPONENT_DEF, component -> {
527                    final DetailAST ident = component.findFirstToken(TokenTypes.IDENT);
528                    componentList.add(ident.getText());
529                });
530        }
531
532        return componentList;
533    }
534}