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     * 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                log(ast, MSG_JAVADOC_MISSING);
219            }
220        }
221    }
222
223    /**
224     * Some javadoc.
225     *
226     * @param methodDef Some javadoc.
227     * @return Some javadoc.
228     */
229    private static int getMethodsNumberOfLine(DetailAST methodDef) {
230        int numberOfLines = 1;
231        final DetailAST lcurly = methodDef.getLastChild();
232        final DetailAST rcurly = lcurly.getLastChild();
233        if (rcurly != null && lcurly.getLineNo() != rcurly.getLineNo()) {
234            numberOfLines = rcurly.getLineNo() - lcurly.getLineNo() - 1;
235        }
236
237        return numberOfLines;
238    }
239
240    /**
241     * Checks if a missing Javadoc is allowed by the check's configuration.
242     *
243     * @param ast the tree node for the method or constructor.
244     * @return True if this method or constructor doesn't need Javadoc.
245     */
246    private boolean isMissingJavadocAllowed(final DetailAST ast) {
247        return allowMissingPropertyJavadoc
248                && (isSetterMethod(ast) || isGetterMethod(ast))
249            || matchesSkipRegex(ast)
250            || isContentsAllowMissingJavadoc(ast);
251    }
252
253    /**
254     * Checks if the Javadoc can be missing if the method or constructor is
255     * below the minimum line count or has a special annotation.
256     *
257     * @param ast the tree node for the method or constructor.
258     * @return True if this method or constructor doesn't need Javadoc.
259     */
260    private boolean isContentsAllowMissingJavadoc(DetailAST ast) {
261        return ast.getType() != TokenTypes.ANNOTATION_FIELD_DEF
262                && (getMethodsNumberOfLine(ast) <= minLineCount
263                    || AnnotationUtil.containsAnnotation(ast, allowedAnnotations));
264    }
265
266    /**
267     * Checks if the given method name matches the regex. In that case
268     * we skip enforcement of javadoc for this method
269     *
270     * @param methodDef {@link TokenTypes#METHOD_DEF METHOD_DEF}
271     * @return true if given method name matches the regex.
272     */
273    private boolean matchesSkipRegex(DetailAST methodDef) {
274        boolean result = false;
275        if (ignoreMethodNamesRegex != null) {
276            final DetailAST ident = methodDef.findFirstToken(TokenTypes.IDENT);
277            final String methodName = ident.getText();
278
279            final Matcher matcher = ignoreMethodNamesRegex.matcher(methodName);
280            if (matcher.matches()) {
281                result = true;
282            }
283        }
284        return result;
285    }
286
287    /**
288     * Whether we should check this node.
289     *
290     * @param ast a given node.
291     * @param nodeScope the scope of the node.
292     * @return whether we should check a given node.
293     */
294    private boolean shouldCheck(final DetailAST ast, final Scope nodeScope) {
295        return ScopeUtil.getSurroundingScope(ast)
296            .map(surroundingScope -> {
297                return nodeScope != excludeScope
298                    && surroundingScope != excludeScope
299                    && nodeScope.isIn(scope)
300                    && surroundingScope.isIn(scope);
301            })
302            .orElse(Boolean.FALSE);
303    }
304
305    /**
306     * Returns whether an AST represents a getter method.
307     *
308     * @param ast the AST to check with
309     * @return whether the AST represents a getter method
310     */
311    public static boolean isGetterMethod(final DetailAST ast) {
312        boolean getterMethod = false;
313
314        // Check have a method with exactly 7 children which are all that
315        // is allowed in a proper getter method which does not throw any
316        // exceptions.
317        if (ast.getType() == TokenTypes.METHOD_DEF
318                && getChildCount(ast) == SETTER_GETTER_MAX_CHILDREN) {
319            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
320            final String name = type.getNextSibling().getText();
321            final boolean matchesGetterFormat = GETTER_PATTERN.matcher(name).matches();
322
323            final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
324            final boolean noParams = params.getChildCount(TokenTypes.PARAMETER_DEF) == 0;
325
326            if (matchesGetterFormat && noParams) {
327                // Now verify that the body consists of:
328                // SLIST -> RETURN
329                // RCURLY
330                final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
331
332                if (slist != null) {
333                    DetailAST expr = slist.getFirstChild();
334                    while (expr.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
335                        expr = expr.getNextSibling();
336                    }
337                    getterMethod = expr.getType() == TokenTypes.LITERAL_RETURN;
338                }
339            }
340        }
341        return getterMethod;
342    }
343
344    /**
345     * Returns whether an AST represents a setter method.
346     *
347     * @param ast the AST to check with
348     * @return whether the AST represents a setter method
349     */
350    public static boolean isSetterMethod(final DetailAST ast) {
351        boolean setterMethod = false;
352
353        // Check have a method with exactly 7 children which are all that
354        // is allowed in a proper setter method which does not throw any
355        // exceptions.
356        if (ast.getType() == TokenTypes.METHOD_DEF
357                && getChildCount(ast) == SETTER_GETTER_MAX_CHILDREN) {
358            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
359            final String name = type.getNextSibling().getText();
360            final boolean matchesSetterFormat = SETTER_PATTERN.matcher(name).matches();
361
362            final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
363            final boolean singleParam = params.getChildCount(TokenTypes.PARAMETER_DEF) == 1;
364
365            if (matchesSetterFormat && singleParam) {
366                // Now verify that the body consists of:
367                // SLIST -> EXPR -> ASSIGN
368                // SEMI
369                // RCURLY
370                final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
371
372                if (slist != null && getChildCount(slist) == SETTER_BODY_SIZE) {
373                    final DetailAST expr = slist.getFirstChild();
374                    setterMethod = expr.getFirstChild().getType() == TokenTypes.ASSIGN;
375                }
376            }
377        }
378        return setterMethod;
379    }
380
381    /**
382     * Returns the number of children without counting comments.
383     *
384     * @param detailAst parent ast
385     * @return the number of children
386     */
387    private static int getChildCount(DetailAST detailAst) {
388        int childCount = 0;
389        DetailAST child = detailAst.getFirstChild();
390
391        while (child != null) {
392            if (child.getType() != TokenTypes.SINGLE_LINE_COMMENT) {
393                childCount += 1;
394            }
395            child = child.getNextSibling();
396        }
397        return childCount;
398    }
399
400}