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 }