001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.Set;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.FileContents;
030import com.puppycrawl.tools.checkstyle.api.Scope;
031import com.puppycrawl.tools.checkstyle.api.TextBlock;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
034import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
035import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
036
037/**
038 * <p>
039 * Checks for missing Javadoc comments for a method or constructor. The scope to verify is
040 * specified using the {@code Scope} class and defaults to {@code Scope.PUBLIC}. To verify
041 * another scope, set property scope to a different
042 * <a href="https://checkstyle.org/property_types.html#Scope">scope</a>.
043 * </p>
044 * <p>
045 * Javadoc is not required on a method that is tagged with the {@code @Override} annotation.
046 * However, under Java 5 it is not possible to mark a method required for an interface (this
047 * was <i>corrected</i> under Java 6). Hence, Checkstyle supports using the convention of using
048 * a single {@code {@inheritDoc}} tag instead of all the other tags.
049 * </p>
050 * <p>
051 * For getters and setters for the property {@code allowMissingPropertyJavadoc}, the methods must
052 * match exactly the structures below.
053 * </p>
054 * <pre>
055 * public void setNumber(final int number)
056 * {
057 *     mNumber = number;
058 * }
059 *
060 * public int getNumber()
061 * {
062 *     return mNumber;
063 * }
064 *
065 * public boolean isSomething()
066 * {
067 *     return false;
068 * }
069 * </pre>
070 * <ul>
071 * <li>
072 * Property {@code allowMissingPropertyJavadoc} - Control whether to allow missing Javadoc on
073 * accessor methods for properties (setters and getters).
074 * Type is {@code boolean}.
075 * Default value is {@code false}.
076 * </li>
077 * <li>
078 * Property {@code allowedAnnotations} - Configure annotations that allow missed
079 * documentation.
080 * Type is {@code java.lang.String[]}.
081 * Default value is {@code Override}.
082 * </li>
083 * <li>
084 * Property {@code excludeScope} - Specify the visibility scope where Javadoc comments are
085 * not checked.
086 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
087 * Default value is {@code null}.
088 * </li>
089 * <li>
090 * Property {@code ignoreMethodNamesRegex} - Ignore method whose names are matching specified
091 * regex.
092 * Type is {@code java.util.regex.Pattern}.
093 * Default value is {@code null}.
094 * </li>
095 * <li>
096 * Property {@code minLineCount} - Control the minimal amount of lines in method to allow no
097 * documentation.
098 * Type is {@code int}.
099 * Default value is {@code -1}.
100 * </li>
101 * <li>
102 * Property {@code scope} - Specify the visibility scope where Javadoc comments are checked.
103 * Type is {@code com.puppycrawl.tools.checkstyle.api.Scope}.
104 * Default value is {@code public}.
105 * </li>
106 * <li>
107 * Property {@code tokens} - tokens to check
108 * Type is {@code java.lang.String[]}.
109 * Validation type is {@code tokenSet}.
110 * Default value is:
111 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
112 * METHOD_DEF</a>,
113 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
114 * CTOR_DEF</a>,
115 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF">
116 * ANNOTATION_FIELD_DEF</a>,
117 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF">
118 * COMPACT_CTOR_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.missing}
130 * </li>
131 * </ul>
132 *
133 * @since 8.21
134 */
135@FileStatefulCheck
136public class MissingJavadocMethodCheck extends AbstractCheck {
137
138    /**
139     * A key is pointing to the warning message text in "messages.properties"
140     * file.
141     */
142    public static final String MSG_JAVADOC_MISSING = "javadoc.missing";
143
144    /** Maximum children allowed in setter/getter. */
145    private static final int SETTER_GETTER_MAX_CHILDREN = 7;
146
147    /** Pattern matching names of getter methods. */
148    private static final Pattern GETTER_PATTERN = Pattern.compile("^(is|get)[A-Z].*");
149
150    /** Pattern matching names of setter methods. */
151    private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*");
152
153    /** Maximum nodes allowed in a body of setter. */
154    private static final int SETTER_BODY_SIZE = 3;
155
156    /** Default value of minimal amount of lines in method to allow no documentation.*/
157    private static final int DEFAULT_MIN_LINE_COUNT = -1;
158
159    /** Specify the visibility scope where Javadoc comments are checked. */
160    private Scope scope = Scope.PUBLIC;
161
162    /** Specify the visibility scope where Javadoc comments are not checked. */
163    private Scope excludeScope;
164
165    /** Control the minimal amount of lines in method to allow no documentation.*/
166    private int minLineCount = DEFAULT_MIN_LINE_COUNT;
167
168    /**
169     * Control whether to allow missing Javadoc on accessor methods for
170     * properties (setters and getters).
171     */
172    private boolean allowMissingPropertyJavadoc;
173
174    /** Ignore method whose names are matching specified regex. */
175    private Pattern ignoreMethodNamesRegex;
176
177    /** Configure annotations that allow missed documentation. */
178    private Set<String> allowedAnnotations = Set.of("Override");
179
180    /**
181     * Setter to configure annotations that allow missed documentation.
182     *
183     * @param userAnnotations user's value.
184     * @since 8.21
185     */
186    public void setAllowedAnnotations(String... userAnnotations) {
187        allowedAnnotations = Set.of(userAnnotations);
188    }
189
190    /**
191     * Setter to ignore method whose names are matching specified regex.
192     *
193     * @param pattern a pattern.
194     * @since 8.21
195     */
196    public void setIgnoreMethodNamesRegex(Pattern pattern) {
197        ignoreMethodNamesRegex = pattern;
198    }
199
200    /**
201     * Setter to control the minimal amount of lines in method to allow no documentation.
202     *
203     * @param value user's value.
204     * @since 8.21
205     */
206    public void setMinLineCount(int value) {
207        minLineCount = value;
208    }
209
210    /**
211     * Setter to control whether to allow missing Javadoc on accessor methods for properties
212     * (setters and getters).
213     *
214     * @param flag a {@code Boolean} value
215     * @since 8.21
216     */
217    public void setAllowMissingPropertyJavadoc(final boolean flag) {
218        allowMissingPropertyJavadoc = flag;
219    }
220
221    /**
222     * Setter to specify the visibility scope where Javadoc comments are checked.
223     *
224     * @param scope a scope.
225     * @since 8.21
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 8.21
236     */
237    public void setExcludeScope(Scope excludeScope) {
238        this.excludeScope = excludeScope;
239    }
240
241    @Override
242    public final int[] getRequiredTokens() {
243        return CommonUtil.EMPTY_INT_ARRAY;
244    }
245
246    @Override
247    public int[] getDefaultTokens() {
248        return getAcceptableTokens();
249    }
250
251    @Override
252    public int[] getAcceptableTokens() {
253        return new int[] {
254            TokenTypes.METHOD_DEF,
255            TokenTypes.CTOR_DEF,
256            TokenTypes.ANNOTATION_FIELD_DEF,
257            TokenTypes.COMPACT_CTOR_DEF,
258        };
259    }
260
261    // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
262    @SuppressWarnings("deprecation")
263    @Override
264    public final void visitToken(DetailAST ast) {
265        final Scope theScope = ScopeUtil.getScope(ast);
266        if (shouldCheck(ast, theScope)) {
267            final FileContents contents = getFileContents();
268            final TextBlock textBlock = contents.getJavadocBefore(ast.getLineNo());
269
270            if (textBlock == null && !isMissingJavadocAllowed(ast)) {
271                log(ast, MSG_JAVADOC_MISSING);
272            }
273        }
274    }
275
276    /**
277     * Some javadoc.
278     *
279     * @param methodDef Some javadoc.
280     * @return Some javadoc.
281     */
282    private static int getMethodsNumberOfLine(DetailAST methodDef) {
283        final int numberOfLines;
284        final DetailAST lcurly = methodDef.getLastChild();
285        final DetailAST rcurly = lcurly.getLastChild();
286
287        if (lcurly.getFirstChild() == rcurly) {
288            numberOfLines = 1;
289        }
290        else {
291            numberOfLines = rcurly.getLineNo() - lcurly.getLineNo() - 1;
292        }
293        return numberOfLines;
294    }
295
296    /**
297     * Checks if a missing Javadoc is allowed by the check's configuration.
298     *
299     * @param ast the tree node for the method or constructor.
300     * @return True if this method or constructor doesn't need Javadoc.
301     */
302    private boolean isMissingJavadocAllowed(final DetailAST ast) {
303        return allowMissingPropertyJavadoc
304                && (isSetterMethod(ast) || isGetterMethod(ast))
305            || matchesSkipRegex(ast)
306            || isContentsAllowMissingJavadoc(ast);
307    }
308
309    /**
310     * Checks if the Javadoc can be missing if the method or constructor is
311     * below the minimum line count or has a special annotation.
312     *
313     * @param ast the tree node for the method or constructor.
314     * @return True if this method or constructor doesn't need Javadoc.
315     */
316    private boolean isContentsAllowMissingJavadoc(DetailAST ast) {
317        return ast.getType() != TokenTypes.ANNOTATION_FIELD_DEF
318                && (getMethodsNumberOfLine(ast) <= minLineCount
319                    || AnnotationUtil.containsAnnotation(ast, allowedAnnotations));
320    }
321
322    /**
323     * Checks if the given method name matches the regex. In that case
324     * we skip enforcement of javadoc for this method
325     *
326     * @param methodDef {@link TokenTypes#METHOD_DEF METHOD_DEF}
327     * @return true if given method name matches the regex.
328     */
329    private boolean matchesSkipRegex(DetailAST methodDef) {
330        boolean result = false;
331        if (ignoreMethodNamesRegex != null) {
332            final DetailAST ident = methodDef.findFirstToken(TokenTypes.IDENT);
333            final String methodName = ident.getText();
334
335            final Matcher matcher = ignoreMethodNamesRegex.matcher(methodName);
336            if (matcher.matches()) {
337                result = true;
338            }
339        }
340        return result;
341    }
342
343    /**
344     * Whether we should check this node.
345     *
346     * @param ast a given node.
347     * @param nodeScope the scope of the node.
348     * @return whether we should check a given node.
349     */
350    private boolean shouldCheck(final DetailAST ast, final Scope nodeScope) {
351        final Scope surroundingScope = ScopeUtil.getSurroundingScope(ast);
352
353        return nodeScope != excludeScope
354                && surroundingScope != excludeScope
355                && nodeScope.isIn(scope)
356                && surroundingScope.isIn(scope);
357    }
358
359    /**
360     * Returns whether an AST represents a getter method.
361     *
362     * @param ast the AST to check with
363     * @return whether the AST represents a getter method
364     */
365    public static boolean isGetterMethod(final DetailAST ast) {
366        boolean getterMethod = false;
367
368        // Check have a method with exactly 7 children which are all that
369        // is allowed in a proper getter method which does not throw any
370        // exceptions.
371        if (ast.getType() == TokenTypes.METHOD_DEF
372                && ast.getChildCount() == SETTER_GETTER_MAX_CHILDREN) {
373            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
374            final String name = type.getNextSibling().getText();
375            final boolean matchesGetterFormat = GETTER_PATTERN.matcher(name).matches();
376
377            final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
378            final boolean noParams = params.getChildCount(TokenTypes.PARAMETER_DEF) == 0;
379
380            if (matchesGetterFormat && noParams) {
381                // Now verify that the body consists of:
382                // SLIST -> RETURN
383                // RCURLY
384                final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
385
386                if (slist != null) {
387                    final DetailAST expr = slist.getFirstChild();
388                    getterMethod = expr.getType() == TokenTypes.LITERAL_RETURN;
389                }
390            }
391        }
392        return getterMethod;
393    }
394
395    /**
396     * Returns whether an AST represents a setter method.
397     *
398     * @param ast the AST to check with
399     * @return whether the AST represents a setter method
400     */
401    public static boolean isSetterMethod(final DetailAST ast) {
402        boolean setterMethod = false;
403
404        // Check have a method with exactly 7 children which are all that
405        // is allowed in a proper setter method which does not throw any
406        // exceptions.
407        if (ast.getType() == TokenTypes.METHOD_DEF
408                && ast.getChildCount() == SETTER_GETTER_MAX_CHILDREN) {
409            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
410            final String name = type.getNextSibling().getText();
411            final boolean matchesSetterFormat = SETTER_PATTERN.matcher(name).matches();
412
413            final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
414            final boolean singleParam = params.getChildCount(TokenTypes.PARAMETER_DEF) == 1;
415
416            if (matchesSetterFormat && singleParam) {
417                // Now verify that the body consists of:
418                // SLIST -> EXPR -> ASSIGN
419                // SEMI
420                // RCURLY
421                final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
422
423                if (slist != null && slist.getChildCount() == SETTER_BODY_SIZE) {
424                    final DetailAST expr = slist.getFirstChild();
425                    setterMethod = expr.getFirstChild().getType() == TokenTypes.ASSIGN;
426                }
427            }
428        }
429        return setterMethod;
430    }
431}