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