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.ScopeUtil;
35  
36  /**
37   * <div>
38   * Checks for missing Javadoc comments for a method or constructor. The scope to verify is
39   * specified using the {@code Scope} class and defaults to {@code Scope.PUBLIC}. To verify
40   * another scope, set property scope to a different
41   * <a href="https://checkstyle.org/property_types.html#Scope">scope</a>.
42   * </div>
43   *
44   * <p>
45   * Javadoc is not required on a method that is tagged with the {@code @Override} annotation.
46   * However, under Java 5 it is not possible to mark a method required for an interface (this
47   * was <i>corrected</i> under Java 6). Hence, Checkstyle supports using the convention of using
48   * a single {@code {@inheritDoc}} tag instead of all the other tags.
49   * </p>
50   *
51   * <p>
52   * For getters and setters for the property {@code allowMissingPropertyJavadoc}, the methods must
53   * match exactly the structures below.
54   * </p>
55   * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
56   * public void setNumber(final int number)
57   * {
58   *     mNumber = number;
59   * }
60   *
61   * public int getNumber()
62   * {
63   *     return mNumber;
64   * }
65   *
66   * public boolean isSomething()
67   * {
68   *     return false;
69   * }
70   * </code></pre></div>
71   *
72   * @since 8.21
73   */
74  @FileStatefulCheck
75  public class MissingJavadocMethodCheck extends AbstractCheck {
76  
77      /**
78       * A key is pointing to the warning message text in "messages.properties"
79       * file.
80       */
81      public static final String MSG_JAVADOC_MISSING = "javadoc.missing";
82  
83      /** Maximum children allowed in setter/getter. */
84      private static final int SETTER_GETTER_MAX_CHILDREN = 7;
85  
86      /** Pattern matching names of getter methods. */
87      private static final Pattern GETTER_PATTERN = Pattern.compile("^(is|get)[A-Z].*");
88  
89      /** Pattern matching names of setter methods. */
90      private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*");
91  
92      /** Maximum nodes allowed in a body of setter. */
93      private static final int SETTER_BODY_SIZE = 3;
94  
95      /** Default value of minimal amount of lines in method to allow no documentation.*/
96      private static final int DEFAULT_MIN_LINE_COUNT = -1;
97  
98      /** Specify the visibility scope where Javadoc comments are checked. */
99      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 }