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