View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.Set;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.Scope;
30  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31  import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
32  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
33  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
34  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
35  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
36  
37  /**
38   * <div>
39   * Checks for missing Javadoc comments for a method or constructor. The scope to verify is
40   * specified using the {@code Scope} class and defaults to {@code Scope.PUBLIC}. To verify
41   * another scope, set property scope to a different
42   * <a href="https://checkstyle.org/property-types.html#Scope">scope</a>.
43   * </div>
44   *
45   * <p>
46   * Javadoc is not required on a method that is tagged with the {@code @Override} annotation.
47   * However, under Java 5 it is not possible to mark a method required for an interface (this
48   * was <i>corrected</i> under Java 6). Hence, Checkstyle supports using the convention of using
49   * a single {@code {@inheritDoc}} tag instead of all the other tags.
50   * </p>
51   *
52   * <p>
53   * For getters and setters for the property {@code allowMissingPropertyJavadoc}, the methods must
54   * match exactly the structures below.
55   * </p>
56   * {@snippet lang="text" :
57   * public void setNumber(final int number)
58   * {
59   *     mNumber = number;
60   * }
61   *
62   * public int getNumber()
63   * {
64   *     return mNumber;
65   * }
66   *
67   * public boolean isSomething()
68   * {
69   *     return false;
70   * }
71   * }
72   *
73   * @since 8.21
74   */
75  @FileStatefulCheck
76  public class MissingJavadocMethodCheck extends AbstractCheck {
77  
78      /**
79       * A key is pointing to the warning message text in "messages.properties"
80       * file.
81       */
82      public static final String MSG_JAVADOC_MISSING = "javadoc.missing.named";
83  
84      /** Maximum children allowed in setter/getter. */
85      private static final int SETTER_GETTER_MAX_CHILDREN = 7;
86  
87      /** Pattern matching names of getter methods. */
88      private static final Pattern GETTER_PATTERN = Pattern.compile("^(is|get)[A-Z].*");
89  
90      /** Pattern matching names of setter methods. */
91      private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*");
92  
93      /** Maximum nodes allowed in a body of setter. */
94      private static final int SETTER_BODY_SIZE = 3;
95  
96      /** Default value of minimal amount of lines in method to allow no documentation.*/
97      private static final int DEFAULT_MIN_LINE_COUNT = -1;
98  
99      /** 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 }