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 *
068 * @since 3.0
069 */
070@StatelessCheck
071public class JavadocTypeCheck
072    extends AbstractCheck {
073
074    /**
075     * A key is pointing to the warning message text in "messages.properties"
076     * file.
077     */
078    public static final String MSG_UNKNOWN_TAG = "javadoc.unknownTag";
079
080    /**
081     * A key is pointing to the warning message text in "messages.properties"
082     * file.
083     */
084    public static final String MSG_TAG_FORMAT = "type.tagFormat";
085
086    /**
087     * A key is pointing to the warning message text in "messages.properties"
088     * file.
089     */
090    public static final String MSG_MISSING_TAG = "type.missingTag";
091
092    /**
093     * A key is pointing to the warning message text in "messages.properties"
094     * file.
095     */
096    public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
097
098    /**
099     * A key is pointing to the warning message text in "messages.properties"
100     * file.
101     */
102    public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
103
104    /** Open angle bracket literal. */
105    private static final String OPEN_ANGLE_BRACKET = "<";
106
107    /** Close angle bracket literal. */
108    private static final String CLOSE_ANGLE_BRACKET = ">";
109
110    /** Space literal. */
111    private static final String SPACE = " ";
112
113    /** Pattern to match type name within angle brackets in javadoc param tag. */
114    private static final Pattern TYPE_NAME_IN_JAVADOC_TAG =
115            Pattern.compile("^<([^>]+)");
116
117    /** Pattern to split type name field in javadoc param tag. */
118    private static final Pattern TYPE_NAME_IN_JAVADOC_TAG_SPLITTER =
119            Pattern.compile("\\s+");
120
121    /** Specify the visibility scope where Javadoc comments are checked. */
122    private Scope scope = Scope.PRIVATE;
123    /** Specify the visibility scope where Javadoc comments are not checked. */
124    private Scope excludeScope;
125    /** Specify the pattern for {@code @author} tag. */
126    private Pattern authorFormat;
127    /** Specify the pattern for {@code @version} tag. */
128    private Pattern versionFormat;
129    /**
130     * Control whether to ignore violations when a class has type parameters but
131     * does not have matching param tags in the Javadoc.
132     */
133    private boolean allowMissingParamTags;
134    /** Control whether to ignore violations when a Javadoc tag is not recognised. */
135    private boolean allowUnknownTags;
136
137    /**
138     * Specify annotations that allow skipping validation at all.
139     * Only short names are allowed, e.g. {@code Generated}.
140     */
141    private Set<String> allowedAnnotations = Set.of("Generated");
142
143    /**
144     * Setter to specify the visibility scope where Javadoc comments are checked.
145     *
146     * @param scope a scope.
147     * @since 3.0
148     */
149    public void setScope(Scope scope) {
150        this.scope = scope;
151    }
152
153    /**
154     * Setter to specify the visibility scope where Javadoc comments are not checked.
155     *
156     * @param excludeScope a scope.
157     * @since 3.4
158     */
159    public void setExcludeScope(Scope excludeScope) {
160        this.excludeScope = excludeScope;
161    }
162
163    /**
164     * Setter to specify the pattern for {@code @author} tag.
165     *
166     * @param pattern a pattern.
167     * @since 3.0
168     */
169    public void setAuthorFormat(Pattern pattern) {
170        authorFormat = pattern;
171    }
172
173    /**
174     * Setter to specify the pattern for {@code @version} tag.
175     *
176     * @param pattern a pattern.
177     * @since 3.0
178     */
179    public void setVersionFormat(Pattern pattern) {
180        versionFormat = pattern;
181    }
182
183    /**
184     * Setter to control whether to ignore violations when a class has type parameters but
185     * does not have matching param tags in the Javadoc.
186     *
187     * @param flag a {@code Boolean} value
188     * @since 4.0
189     */
190    public void setAllowMissingParamTags(boolean flag) {
191        allowMissingParamTags = flag;
192    }
193
194    /**
195     * Setter to control whether to ignore violations when a Javadoc tag is not recognised.
196     *
197     * @param flag a {@code Boolean} value
198     * @since 5.1
199     */
200    public void setAllowUnknownTags(boolean flag) {
201        allowUnknownTags = flag;
202    }
203
204    /**
205     * Setter to specify annotations that allow skipping validation at all.
206     * Only short names are allowed, e.g. {@code Generated}.
207     *
208     * @param userAnnotations user's value.
209     * @since 8.15
210     */
211    public void setAllowedAnnotations(String... userAnnotations) {
212        allowedAnnotations = Set.of(userAnnotations);
213    }
214
215    @Override
216    public int[] getDefaultTokens() {
217        return getAcceptableTokens();
218    }
219
220    @Override
221    public int[] getAcceptableTokens() {
222        return new int[] {
223            TokenTypes.INTERFACE_DEF,
224            TokenTypes.CLASS_DEF,
225            TokenTypes.ENUM_DEF,
226            TokenTypes.ANNOTATION_DEF,
227            TokenTypes.RECORD_DEF,
228        };
229    }
230
231    @Override
232    public int[] getRequiredTokens() {
233        return CommonUtil.EMPTY_INT_ARRAY;
234    }
235
236    // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
237    @Override
238    @SuppressWarnings("deprecation")
239    public void visitToken(DetailAST ast) {
240        if (shouldCheck(ast)) {
241            final FileContents contents = getFileContents();
242            final int lineNo = ast.getLineNo();
243            final TextBlock textBlock = contents.getJavadocBefore(lineNo);
244            if (textBlock != null) {
245                final List<JavadocTag> tags = getJavadocTags(textBlock);
246                if (ScopeUtil.isOuterMostType(ast)) {
247                    // don't check author/version for inner classes
248                    checkTag(ast, tags, JavadocTagInfo.AUTHOR.getName(),
249                            authorFormat);
250                    checkTag(ast, tags, JavadocTagInfo.VERSION.getName(),
251                            versionFormat);
252                }
253
254                final List<String> typeParamNames =
255                    CheckUtil.getTypeParameterNames(ast);
256                final List<String> recordComponentNames =
257                    getRecordComponentNames(ast);
258
259                if (!allowMissingParamTags) {
260
261                    typeParamNames.forEach(typeParamName -> {
262                        checkTypeParamTag(ast, tags, typeParamName);
263                    });
264
265                    recordComponentNames.forEach(componentName -> {
266                        checkComponentParamTag(ast, tags, componentName);
267                    });
268                }
269
270                checkUnusedParamTags(tags, typeParamNames, recordComponentNames);
271            }
272        }
273    }
274
275    /**
276     * Whether we should check this node.
277     *
278     * @param ast a given node.
279     * @return whether we should check a given node.
280     */
281    private boolean shouldCheck(DetailAST ast) {
282        final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
283
284        return surroundingScope.isIn(scope)
285                && (excludeScope == null || !surroundingScope.isIn(excludeScope))
286                && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations);
287    }
288
289    /**
290     * Gets all standalone tags from a given javadoc.
291     *
292     * @param textBlock the Javadoc comment to process.
293     * @return all standalone tags from the given javadoc.
294     */
295    private List<JavadocTag> getJavadocTags(TextBlock textBlock) {
296        final JavadocTags tags = JavadocUtil.getJavadocTags(textBlock,
297            JavadocUtil.JavadocTagType.BLOCK);
298        if (!allowUnknownTags) {
299            tags.getInvalidTags().forEach(tag -> {
300                log(tag.getLine(), tag.getCol(), MSG_UNKNOWN_TAG, tag.getName());
301            });
302        }
303        return tags.getValidTags();
304    }
305
306    /**
307     * Verifies that a type definition has a required tag.
308     *
309     * @param ast the AST node for the type definition.
310     * @param tags tags from the Javadoc comment for the type definition.
311     * @param tagName the required tag name.
312     * @param formatPattern regexp for the tag value.
313     */
314    private void checkTag(DetailAST ast, Iterable<JavadocTag> tags, String tagName,
315                          Pattern formatPattern) {
316        if (formatPattern != null) {
317            boolean hasTag = false;
318            final String tagPrefix = "@";
319
320            for (final JavadocTag tag :tags) {
321                if (tag.getTagName().equals(tagName)) {
322                    hasTag = true;
323                    if (!formatPattern.matcher(tag.getFirstArg()).find()) {
324                        log(ast, MSG_TAG_FORMAT, tagPrefix + tagName, formatPattern.pattern());
325                    }
326                }
327            }
328            if (!hasTag) {
329                log(ast, MSG_MISSING_TAG, tagPrefix + tagName);
330            }
331        }
332    }
333
334    /**
335     * Verifies that a record definition has the specified param tag for
336     * the specified record component name.
337     *
338     * @param ast the AST node for the record definition.
339     * @param tags tags from the Javadoc comment for the record definition.
340     * @param recordComponentName the name of the type parameter
341     */
342    private void checkComponentParamTag(DetailAST ast,
343                                        Collection<JavadocTag> tags,
344                                        String recordComponentName) {
345
346        final boolean found = tags
347            .stream()
348            .filter(JavadocTag::isParamTag)
349            .anyMatch(tag -> tag.getFirstArg().indexOf(recordComponentName) == 0);
350
351        if (!found) {
352            log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
353                + SPACE + recordComponentName);
354        }
355    }
356
357    /**
358     * Verifies that a type definition has the specified param tag for
359     * the specified type parameter name.
360     *
361     * @param ast the AST node for the type definition.
362     * @param tags tags from the Javadoc comment for the type definition.
363     * @param typeParamName the name of the type parameter
364     */
365    private void checkTypeParamTag(DetailAST ast,
366            Collection<JavadocTag> tags, String typeParamName) {
367        final String typeParamNameWithBrackets =
368            OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET;
369
370        final boolean found = tags
371            .stream()
372            .filter(JavadocTag::isParamTag)
373            .anyMatch(tag -> tag.getFirstArg().indexOf(typeParamNameWithBrackets) == 0);
374
375        if (!found) {
376            log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
377                + SPACE + typeParamNameWithBrackets);
378        }
379    }
380
381    /**
382     * Checks for unused param tags for type parameters and record components.
383     *
384     * @param tags tags from the Javadoc comment for the type definition
385     * @param typeParamNames names of type parameters
386     * @param recordComponentNames record component names in this definition
387     */
388    private void checkUnusedParamTags(
389        List<JavadocTag> tags,
390        List<String> typeParamNames,
391        List<String> recordComponentNames) {
392
393        for (final JavadocTag tag: tags) {
394            if (tag.isParamTag()) {
395                final String paramName = extractParamNameFromTag(tag);
396                final boolean found = typeParamNames.contains(paramName)
397                        || recordComponentNames.contains(paramName);
398
399                if (!found) {
400                    if (paramName.isEmpty()) {
401                        log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG_GENERAL);
402                    }
403                    else {
404                        final String actualParamName =
405                            TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0];
406                        log(tag.getLineNo(), tag.getColumnNo(),
407                            MSG_UNUSED_TAG,
408                            JavadocTagInfo.PARAM.getText(), actualParamName);
409                    }
410                }
411            }
412        }
413
414    }
415
416    /**
417     * Extracts parameter name from tag.
418     *
419     * @param tag javadoc tag to extract parameter name
420     * @return extracts type parameter name from tag
421     */
422    private static String extractParamNameFromTag(JavadocTag tag) {
423        final String typeParamName;
424        final Matcher matchInAngleBrackets =
425                TYPE_NAME_IN_JAVADOC_TAG.matcher(tag.getFirstArg());
426        if (matchInAngleBrackets.find()) {
427            typeParamName = matchInAngleBrackets.group(1).trim();
428        }
429        else {
430            typeParamName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0];
431        }
432        return typeParamName;
433    }
434
435    /**
436     * Collects the record components in a record definition.
437     *
438     * @param node the possible record definition ast.
439     * @return the record components in this record definition.
440     */
441    private static List<String> getRecordComponentNames(DetailAST node) {
442        final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS);
443        final List<String> componentList = new ArrayList<>();
444
445        if (components != null) {
446            TokenUtil.forEachChild(components,
447                TokenTypes.RECORD_COMPONENT_DEF, component -> {
448                    final DetailAST ident = component.findFirstToken(TokenTypes.IDENT);
449                    componentList.add(ident.getText());
450                });
451        }
452
453        return componentList;
454    }
455}