001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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.Arrays;
024import java.util.Collections;
025import java.util.List;
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 * regular expression</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 * Error messages about type parameters and record components for which no param tags are present
059 * can be suppressed by defining property {@code allowMissingParamTags}.
060 * </p>
061 * <ul>
062 * <li>
063 * Property {@code scope} - Specify the visibility scope where Javadoc comments are checked.
064 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
065 * Default value is {@code private}.
066 * </li>
067 * <li>
068 * Property {@code excludeScope} - Specify the visibility scope where Javadoc
069 * comments are not checked.
070 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
071 * Default value is {@code null}.
072 * </li>
073 * <li>
074 * Property {@code authorFormat} - Specify the pattern for {@code @author} tag.
075 * Type is {@code java.util.regex.Pattern}.
076 * Default value is {@code null}.
077 * </li>
078 * <li>
079 * Property {@code versionFormat} - Specify the pattern for {@code @version} tag.
080 * Type is {@code java.util.regex.Pattern}.
081 * Default value is {@code null}.
082 * </li>
083 * <li>
084 * Property {@code allowMissingParamTags} - Control whether to ignore violations
085 * when a class has type parameters but does not have matching param tags in the Javadoc.
086 * Type is {@code boolean}.
087 * Default value is {@code false}.
088 * </li>
089 * <li>
090 * Property {@code allowUnknownTags} - Control whether to ignore violations when
091 * a Javadoc tag is not recognised.
092 * Type is {@code boolean}.
093 * Default value is {@code false}.
094 * </li>
095 * <li>
096 * Property {@code allowedAnnotations} - Specify the list of annotations that allow
097 * missed documentation. Only short names are allowed, e.g. {@code Generated}.
098 * Type is {@code java.lang.String[]}.
099 * Default value is {@code Generated}.
100 * </li>
101 * <li>
102 * Property {@code tokens} - tokens to check
103 * Type is {@code java.lang.String[]}.
104 * Validation type is {@code tokenSet}.
105 * Default value is:
106 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INTERFACE_DEF">
107 * INTERFACE_DEF</a>,
108 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CLASS_DEF">
109 * CLASS_DEF</a>,
110 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ENUM_DEF">
111 * ENUM_DEF</a>,
112 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_DEF">
113 * ANNOTATION_DEF</a>,
114 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#RECORD_DEF">
115 * RECORD_DEF</a>.
116 * </li>
117 * </ul>
118 * <p>
119 * To configure the default check:
120 * </p>
121 * <pre>
122 * &lt;module name="JavadocType"/&gt;
123 * </pre>
124 * <p>
125 * To configure the check for {@code public} scope:
126 * </p>
127 * <pre>
128 * &lt;module name="JavadocType"&gt;
129 *   &lt;property name="scope" value="public"/&gt;
130 * &lt;/module&gt;
131 * </pre>
132 * <p>
133 * To configure the check for an {@code @author} tag:
134 * </p>
135 * <pre>
136 * &lt;module name="JavadocType"&gt;
137 *   &lt;property name="authorFormat" value="\S"/&gt;
138 * &lt;/module&gt;
139 * </pre>
140 * <p>
141 * To configure the check for a CVS revision version tag:
142 * </p>
143 * <pre>
144 * &lt;module name="JavadocType"&gt;
145 *   &lt;property name="versionFormat" value="\$Revision.*\$"/&gt;
146 * &lt;/module&gt;
147 * </pre>
148 * <p>
149 * To configure the check for {@code private} classes only:
150 * </p>
151 * <pre>
152 * &lt;module name="JavadocType"&gt;
153 *   &lt;property name="scope" value="private"/&gt;
154 *   &lt;property name="excludeScope" value="package"/&gt;
155 * &lt;/module&gt;
156 * </pre>
157 * <p>
158 * Example that allows missing comments for classes annotated with
159 * {@code @SpringBootApplication} and {@code @Configuration}:
160 * </p>
161 * <pre>
162 * &#64;SpringBootApplication // no violations about missing comment on class
163 * public class Application {}
164 *
165 * &#64;Configuration // no violations about missing comment on class
166 * class DatabaseConfiguration {}
167 * </pre>
168 * <p>
169 * Use following configuration:
170 * </p>
171 * <pre>
172 * &lt;module name="JavadocType"&gt;
173 *   &lt;property name="allowedAnnotations" value="SpringBootApplication,Configuration"/&gt;
174 * &lt;/module&gt;
175 * </pre>
176 * <p>
177 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
178 * </p>
179 * <p>
180 * Violation Message Keys:
181 * </p>
182 * <ul>
183 * <li>
184 * {@code javadoc.unknownTag}
185 * </li>
186 * <li>
187 * {@code javadoc.unusedTag}
188 * </li>
189 * <li>
190 * {@code javadoc.unusedTagGeneral}
191 * </li>
192 * <li>
193 * {@code type.missingTag}
194 * </li>
195 * <li>
196 * {@code type.tagFormat}
197 * </li>
198 * </ul>
199 *
200 * @since 3.0
201 *
202 */
203@StatelessCheck
204public class JavadocTypeCheck
205    extends AbstractCheck {
206
207    /**
208     * A key is pointing to the warning message text in "messages.properties"
209     * file.
210     */
211    public static final String MSG_UNKNOWN_TAG = "javadoc.unknownTag";
212
213    /**
214     * A key is pointing to the warning message text in "messages.properties"
215     * file.
216     */
217    public static final String MSG_TAG_FORMAT = "type.tagFormat";
218
219    /**
220     * A key is pointing to the warning message text in "messages.properties"
221     * file.
222     */
223    public static final String MSG_MISSING_TAG = "type.missingTag";
224
225    /**
226     * A key is pointing to the warning message text in "messages.properties"
227     * file.
228     */
229    public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
230
231    /**
232     * A key is pointing to the warning message text in "messages.properties"
233     * file.
234     */
235    public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
236
237    /** Open angle bracket literal. */
238    private static final String OPEN_ANGLE_BRACKET = "<";
239
240    /** Close angle bracket literal. */
241    private static final String CLOSE_ANGLE_BRACKET = ">";
242
243    /** Space literal. */
244    private static final String SPACE = " ";
245
246    /** Pattern to match type name within angle brackets in javadoc param tag. */
247    private static final Pattern TYPE_NAME_IN_JAVADOC_TAG =
248            Pattern.compile("\\s*<([^>]+)>.*");
249
250    /** Pattern to split type name field in javadoc param tag. */
251    private static final Pattern TYPE_NAME_IN_JAVADOC_TAG_SPLITTER =
252            Pattern.compile("\\s+");
253
254    /** Specify the visibility scope where Javadoc comments are checked. */
255    private Scope scope = Scope.PRIVATE;
256    /** Specify the visibility scope where Javadoc comments are not checked. */
257    private Scope excludeScope;
258    /** Specify the pattern for {@code @author} tag. */
259    private Pattern authorFormat;
260    /** Specify the pattern for {@code @version} tag. */
261    private Pattern versionFormat;
262    /**
263     * Control whether to ignore violations when a class has type parameters but
264     * does not have matching param tags in the Javadoc.
265     */
266    private boolean allowMissingParamTags;
267    /** Control whether to ignore violations when a Javadoc tag is not recognised. */
268    private boolean allowUnknownTags;
269
270    /**
271     * Specify the list of annotations that allow missed documentation.
272     * Only short names are allowed, e.g. {@code Generated}.
273     */
274    private List<String> allowedAnnotations = Collections.singletonList("Generated");
275
276    /**
277     * Setter to specify the visibility scope where Javadoc comments are checked.
278     *
279     * @param scope a scope.
280     */
281    public void setScope(Scope scope) {
282        this.scope = scope;
283    }
284
285    /**
286     * Setter to specify the visibility scope where Javadoc comments are not checked.
287     *
288     * @param excludeScope a scope.
289     */
290    public void setExcludeScope(Scope excludeScope) {
291        this.excludeScope = excludeScope;
292    }
293
294    /**
295     * Setter to specify the pattern for {@code @author} tag.
296     *
297     * @param pattern a pattern.
298     */
299    public void setAuthorFormat(Pattern pattern) {
300        authorFormat = pattern;
301    }
302
303    /**
304     * Setter to specify the pattern for {@code @version} tag.
305     *
306     * @param pattern a pattern.
307     */
308    public void setVersionFormat(Pattern pattern) {
309        versionFormat = pattern;
310    }
311
312    /**
313     * Setter to control whether to ignore violations when a class has type parameters but
314     * does not have matching param tags in the Javadoc.
315     *
316     * @param flag a {@code Boolean} value
317     */
318    public void setAllowMissingParamTags(boolean flag) {
319        allowMissingParamTags = flag;
320    }
321
322    /**
323     * Setter to control whether to ignore violations when a Javadoc tag is not recognised.
324     *
325     * @param flag a {@code Boolean} value
326     */
327    public void setAllowUnknownTags(boolean flag) {
328        allowUnknownTags = flag;
329    }
330
331    /**
332     * Setter to specify the list of annotations that allow missed documentation.
333     * Only short names are allowed, e.g. {@code Generated}.
334     *
335     * @param userAnnotations user's value.
336     */
337    public void setAllowedAnnotations(String... userAnnotations) {
338        allowedAnnotations = Arrays.asList(userAnnotations);
339    }
340
341    @Override
342    public int[] getDefaultTokens() {
343        return getAcceptableTokens();
344    }
345
346    @Override
347    public int[] getAcceptableTokens() {
348        return new int[] {
349            TokenTypes.INTERFACE_DEF,
350            TokenTypes.CLASS_DEF,
351            TokenTypes.ENUM_DEF,
352            TokenTypes.ANNOTATION_DEF,
353            TokenTypes.RECORD_DEF,
354        };
355    }
356
357    @Override
358    public int[] getRequiredTokens() {
359        return CommonUtil.EMPTY_INT_ARRAY;
360    }
361
362    @Override
363    public void visitToken(DetailAST ast) {
364        if (shouldCheck(ast)) {
365            final FileContents contents = getFileContents();
366            final int lineNo = ast.getLineNo();
367            final TextBlock textBlock = contents.getJavadocBefore(lineNo);
368            if (textBlock != null) {
369                final List<JavadocTag> tags = getJavadocTags(textBlock);
370                if (ScopeUtil.isOuterMostType(ast)) {
371                    // don't check author/version for inner classes
372                    checkTag(ast, tags, JavadocTagInfo.AUTHOR.getName(),
373                            authorFormat);
374                    checkTag(ast, tags, JavadocTagInfo.VERSION.getName(),
375                            versionFormat);
376                }
377
378                final List<String> typeParamNames =
379                    CheckUtil.getTypeParameterNames(ast);
380                final List<String> recordComponentNames =
381                    getRecordComponentNames(ast);
382
383                if (!allowMissingParamTags) {
384
385                    typeParamNames.forEach(typeParamName -> {
386                        checkTypeParamTag(ast, tags, typeParamName);
387                    });
388
389                    recordComponentNames.forEach(componentName -> {
390                        checkComponentParamTag(ast, tags, componentName);
391                    });
392                }
393
394                checkUnusedParamTags(tags, typeParamNames, recordComponentNames);
395            }
396        }
397    }
398
399    /**
400     * Whether we should check this node.
401     *
402     * @param ast a given node.
403     * @return whether we should check a given node.
404     */
405    private boolean shouldCheck(DetailAST ast) {
406        final Scope customScope;
407
408        if (ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
409            customScope = Scope.PUBLIC;
410        }
411        else {
412            final DetailAST mods = ast.findFirstToken(TokenTypes.MODIFIERS);
413            customScope = ScopeUtil.getScopeFromMods(mods);
414        }
415        final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
416
417        return customScope.isIn(scope)
418            && (surroundingScope == null || surroundingScope.isIn(scope))
419            && (excludeScope == null
420                || !customScope.isIn(excludeScope)
421                || surroundingScope != null
422                && !surroundingScope.isIn(excludeScope))
423            && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations);
424    }
425
426    /**
427     * Gets all standalone tags from a given javadoc.
428     *
429     * @param textBlock the Javadoc comment to process.
430     * @return all standalone tags from the given javadoc.
431     */
432    private List<JavadocTag> getJavadocTags(TextBlock textBlock) {
433        final JavadocTags tags = JavadocUtil.getJavadocTags(textBlock,
434            JavadocUtil.JavadocTagType.BLOCK);
435        if (!allowUnknownTags) {
436            for (final InvalidJavadocTag tag : tags.getInvalidTags()) {
437                log(tag.getLine(), tag.getCol(), MSG_UNKNOWN_TAG,
438                    tag.getName());
439            }
440        }
441        return tags.getValidTags();
442    }
443
444    /**
445     * Verifies that a type definition has a required tag.
446     *
447     * @param ast the AST node for the type definition.
448     * @param tags tags from the Javadoc comment for the type definition.
449     * @param tagName the required tag name.
450     * @param formatPattern regexp for the tag value.
451     */
452    private void checkTag(DetailAST ast, List<JavadocTag> tags, String tagName,
453                          Pattern formatPattern) {
454        if (formatPattern != null) {
455            boolean hasTag = false;
456            final String tagPrefix = "@";
457
458            for (final JavadocTag tag :tags) {
459                if (tag.getTagName().equals(tagName)) {
460                    hasTag = true;
461                    if (!formatPattern.matcher(tag.getFirstArg()).find()) {
462                        log(ast, MSG_TAG_FORMAT, tagPrefix + tagName, formatPattern.pattern());
463                    }
464                }
465            }
466            if (!hasTag) {
467                log(ast, MSG_MISSING_TAG, tagPrefix + tagName);
468            }
469        }
470    }
471
472    /**
473     * Verifies that a record definition has the specified param tag for
474     * the specified record component name.
475     *
476     * @param ast the AST node for the record definition.
477     * @param tags tags from the Javadoc comment for the record definition.
478     * @param recordComponentName the name of the type parameter
479     */
480    private void checkComponentParamTag(DetailAST ast,
481                                        List<JavadocTag> tags,
482                                        String recordComponentName) {
483
484        final boolean found = tags
485            .stream()
486            .filter(JavadocTag::isParamTag)
487            .anyMatch(tag -> tag.getFirstArg().indexOf(recordComponentName) == 0);
488
489        if (!found) {
490            log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
491                + SPACE + recordComponentName);
492        }
493    }
494
495    /**
496     * Verifies that a type definition has the specified param tag for
497     * the specified type parameter name.
498     *
499     * @param ast the AST node for the type definition.
500     * @param tags tags from the Javadoc comment for the type definition.
501     * @param typeParamName the name of the type parameter
502     */
503    private void checkTypeParamTag(DetailAST ast,
504            List<JavadocTag> tags, String typeParamName) {
505        final String typeParamNameWithBrackets =
506            OPEN_ANGLE_BRACKET + typeParamName + CLOSE_ANGLE_BRACKET;
507
508        final boolean found = tags
509            .stream()
510            .filter(JavadocTag::isParamTag)
511            .anyMatch(tag -> tag.getFirstArg().indexOf(typeParamNameWithBrackets) == 0);
512
513        if (!found) {
514            log(ast, MSG_MISSING_TAG, JavadocTagInfo.PARAM.getText()
515                + SPACE + typeParamNameWithBrackets);
516        }
517    }
518
519    /**
520     * Checks for unused param tags for type parameters and record components.
521     *
522     * @param tags tags from the Javadoc comment for the type definition.
523     * @param typeParamNames names of type parameters
524     * @param recordComponentNames list of record component names in this definition
525     */
526    private void checkUnusedParamTags(
527        List<JavadocTag> tags,
528        List<String> typeParamNames,
529        List<String> recordComponentNames) {
530
531        for (final JavadocTag tag: tags) {
532            if (tag.isParamTag()) {
533                final String paramName = extractParamNameFromTag(tag);
534                final boolean found = typeParamNames.contains(paramName)
535                        || recordComponentNames.contains(paramName);
536
537                if (!found) {
538                    final String actualParamName =
539                        TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0];
540                    log(tag.getLineNo(), tag.getColumnNo(),
541                        MSG_UNUSED_TAG,
542                        JavadocTagInfo.PARAM.getText(), actualParamName);
543                }
544            }
545        }
546
547    }
548
549    /**
550     * Extracts parameter name from tag.
551     *
552     * @param tag javadoc tag to extract parameter name
553     * @return extracts type parameter name from tag
554     */
555    private static String extractParamNameFromTag(JavadocTag tag) {
556        final String typeParamName;
557        final Matcher matchInAngleBrackets =
558                TYPE_NAME_IN_JAVADOC_TAG.matcher(tag.getFirstArg());
559        if (matchInAngleBrackets.find()) {
560            typeParamName = matchInAngleBrackets.group(1).trim();
561        }
562        else {
563            typeParamName = TYPE_NAME_IN_JAVADOC_TAG_SPLITTER.split(tag.getFirstArg())[0];
564        }
565        return typeParamName;
566    }
567
568    /**
569     * Collects the record components in a record definition.
570     *
571     * @param node the possible record definition ast.
572     * @return the list of record components in this record definition.
573     */
574    private static List<String> getRecordComponentNames(DetailAST node) {
575        final DetailAST components = node.findFirstToken(TokenTypes.RECORD_COMPONENTS);
576        final List<String> componentList = new ArrayList<>();
577
578        if (components != null) {
579            TokenUtil.forEachChild(components,
580                TokenTypes.RECORD_COMPONENT_DEF, component -> {
581                    final DetailAST ident = component.findFirstToken(TokenTypes.IDENT);
582                    componentList.add(ident.getText());
583                });
584        }
585
586        return componentList;
587    }
588}