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.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 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
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 * </code></pre></div>
072 *
073 * @since 8.21
074 */
075@FileStatefulCheck
076public class MissingJavadocMethodCheck extends AbstractCheck {
077
078    /**
079     * A key is pointing to the warning message text in "messages.properties"
080     * file.
081     */
082    public static final String MSG_JAVADOC_MISSING = "javadoc.missing.named";
083
084    /** Maximum children allowed in setter/getter. */
085    private static final int SETTER_GETTER_MAX_CHILDREN = 7;
086
087    /** Pattern matching names of getter methods. */
088    private static final Pattern GETTER_PATTERN = Pattern.compile("^(is|get)[A-Z].*");
089
090    /** Pattern matching names of setter methods. */
091    private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*");
092
093    /** Maximum nodes allowed in a body of setter. */
094    private static final int SETTER_BODY_SIZE = 3;
095
096    /** Default value of minimal amount of lines in method to allow no documentation.*/
097    private static final int DEFAULT_MIN_LINE_COUNT = -1;
098
099    /** Specify the visibility scope where Javadoc comments are checked. */
100    private Scope scope = Scope.PUBLIC;
101
102    /** Specify the visibility scope where Javadoc comments are not checked. */
103    private Scope excludeScope;
104
105    /** Control the minimal amount of lines in method to allow no documentation.*/
106    private int minLineCount = DEFAULT_MIN_LINE_COUNT;
107
108    /**
109     * Control whether to allow missing Javadoc on accessor methods for
110     * properties (setters and getters).
111     */
112    private boolean allowMissingPropertyJavadoc;
113
114    /** Ignore method whose names are matching specified regex. */
115    private Pattern ignoreMethodNamesRegex;
116
117    /** Configure annotations that allow missed documentation. */
118    private Set<String> allowedAnnotations = Set.of("Override");
119
120    /**
121     * Creates a new {@code MissingJavadocMethodCheck} instance.
122     */
123    public MissingJavadocMethodCheck() {
124        // no code by default
125    }
126
127    /**
128     * Setter to configure annotations that allow missed documentation.
129     *
130     * @param userAnnotations user's value.
131     * @since 8.21
132     */
133    public void setAllowedAnnotations(String... userAnnotations) {
134        allowedAnnotations = Set.of(userAnnotations);
135    }
136
137    /**
138     * Setter to ignore method whose names are matching specified regex.
139     *
140     * @param pattern a pattern.
141     * @since 8.21
142     */
143    public void setIgnoreMethodNamesRegex(Pattern pattern) {
144        ignoreMethodNamesRegex = pattern;
145    }
146
147    /**
148     * Setter to control the minimal amount of lines in method to allow no documentation.
149     *
150     * @param value user's value.
151     * @since 8.21
152     */
153    public void setMinLineCount(int value) {
154        minLineCount = value;
155    }
156
157    /**
158     * Setter to control whether to allow missing Javadoc on accessor methods for properties
159     * (setters and getters).
160     *
161     * @param flag a {@code Boolean} value
162     * @since 8.21
163     */
164    public void setAllowMissingPropertyJavadoc(final boolean flag) {
165        allowMissingPropertyJavadoc = flag;
166    }
167
168    /**
169     * Setter to specify the visibility scope where Javadoc comments are checked.
170     *
171     * @param scope a scope.
172     * @since 8.21
173     */
174    public void setScope(Scope scope) {
175        this.scope = scope;
176    }
177
178    /**
179     * Setter to specify the visibility scope where Javadoc comments are not checked.
180     *
181     * @param excludeScope a scope.
182     * @since 8.21
183     */
184    public void setExcludeScope(Scope excludeScope) {
185        this.excludeScope = excludeScope;
186    }
187
188    @Override
189    public final int[] getRequiredTokens() {
190        return CommonUtil.EMPTY_INT_ARRAY;
191    }
192
193    @Override
194    public int[] getDefaultTokens() {
195        return getAcceptableTokens();
196    }
197
198    @Override
199    public int[] getAcceptableTokens() {
200        return new int[] {
201            TokenTypes.METHOD_DEF,
202            TokenTypes.CTOR_DEF,
203            TokenTypes.ANNOTATION_FIELD_DEF,
204            TokenTypes.COMPACT_CTOR_DEF,
205        };
206    }
207
208    @Override
209    public boolean isCommentNodesRequired() {
210        return true;
211    }
212
213    @Override
214    public final void visitToken(DetailAST ast) {
215        final Scope theScope = ScopeUtil.getScope(ast);
216        if (shouldCheck(ast, theScope)) {
217            final DetailAST blockCommentNode = JavadocUtil.getAttachedJavadocComment(ast);
218            if (blockCommentNode == null && !isMissingJavadocAllowed(ast)) {
219                final String name = NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT))
220                    .getText();
221                log(ast, MSG_JAVADOC_MISSING, name);
222            }
223        }
224    }
225
226    /**
227     * Some javadoc.
228     *
229     * @param methodDef Some javadoc.
230     * @return Some javadoc.
231     */
232    private static int getMethodsNumberOfLine(DetailAST methodDef) {
233        int numberOfLines = 1;
234        final DetailAST lcurly = methodDef.getLastChild();
235        final DetailAST rcurly = lcurly.getLastChild();
236        if (rcurly != null && lcurly.getLineNo() != rcurly.getLineNo()) {
237            numberOfLines = rcurly.getLineNo() - lcurly.getLineNo() - 1;
238        }
239
240        return numberOfLines;
241    }
242
243    /**
244     * Checks if a missing Javadoc is allowed by the check's configuration.
245     *
246     * @param ast the tree node for the method or constructor.
247     * @return True if this method or constructor doesn't need Javadoc.
248     */
249    private boolean isMissingJavadocAllowed(final DetailAST ast) {
250        return allowMissingPropertyJavadoc
251                && (isSetterMethod(ast) || isGetterMethod(ast))
252            || matchesSkipRegex(ast)
253            || isContentsAllowMissingJavadoc(ast);
254    }
255
256    /**
257     * Checks if the Javadoc can be missing if the method or constructor is
258     * below the minimum line count or has a special annotation.
259     *
260     * @param ast the tree node for the method or constructor.
261     * @return True if this method or constructor doesn't need Javadoc.
262     */
263    private boolean isContentsAllowMissingJavadoc(DetailAST ast) {
264        return ast.getType() != TokenTypes.ANNOTATION_FIELD_DEF
265                && (getMethodsNumberOfLine(ast) <= minLineCount
266                    || AnnotationUtil.containsAnnotation(ast, allowedAnnotations));
267    }
268
269    /**
270     * Checks if the given method name matches the regex. In that case
271     * we skip enforcement of javadoc for this method
272     *
273     * @param methodDef {@link TokenTypes#METHOD_DEF METHOD_DEF}
274     * @return true if given method name matches the regex.
275     */
276    private boolean matchesSkipRegex(DetailAST methodDef) {
277        boolean result = false;
278        if (ignoreMethodNamesRegex != null) {
279            final DetailAST ident = methodDef.findFirstToken(TokenTypes.IDENT);
280            final String methodName = ident.getText();
281
282            final Matcher matcher = ignoreMethodNamesRegex.matcher(methodName);
283            if (matcher.matches()) {
284                result = true;
285            }
286        }
287        return result;
288    }
289
290    /**
291     * Whether we should check this node.
292     *
293     * @param ast a given node.
294     * @param nodeScope the scope of the node.
295     * @return whether we should check a given node.
296     */
297    private boolean shouldCheck(final DetailAST ast, final Scope nodeScope) {
298        return ScopeUtil.getSurroundingScope(ast)
299            .map(surroundingScope -> {
300                return nodeScope != excludeScope
301                    && surroundingScope != excludeScope
302                    && nodeScope.isIn(scope)
303                    && surroundingScope.isIn(scope);
304            })
305            .orElse(Boolean.FALSE);
306    }
307
308    /**
309     * Returns whether an AST represents a getter method.
310     *
311     * @param ast the AST to check with
312     * @return whether the AST represents a getter method
313     */
314    public static boolean isGetterMethod(final DetailAST ast) {
315        boolean getterMethod = false;
316
317        // Check have a method with exactly 7 children which are all that
318        // is allowed in a proper getter method which does not throw any
319        // exceptions.
320        if (ast.getType() == TokenTypes.METHOD_DEF
321                && getChildCount(ast) == SETTER_GETTER_MAX_CHILDREN) {
322            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
323            final String name = type.getNextSibling().getText();
324            final boolean matchesGetterFormat = GETTER_PATTERN.matcher(name).matches();
325
326            final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
327            final boolean noParams = params.getChildCount(TokenTypes.PARAMETER_DEF) == 0;
328
329            if (matchesGetterFormat && noParams) {
330                // Now verify that the body consists of:
331                // SLIST -> RETURN
332                // RCURLY
333                final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
334
335                if (slist != null) {
336                    DetailAST expr = slist.getFirstChild();
337                    while (expr.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
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                childCount += 1;
397            }
398            child = child.getNextSibling();
399        }
400        return childCount;
401    }
402
403}