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.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.ListIterator;
28 import java.util.Optional;
29 import java.util.Set;
30
31 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
32 import com.puppycrawl.tools.checkstyle.api.DetailAST;
33 import com.puppycrawl.tools.checkstyle.api.DetailNode;
34 import com.puppycrawl.tools.checkstyle.api.FullIdent;
35 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
36 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37 import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
38 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
39 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
40 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
41 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
42 import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;
43
44 /**
45 * <div>
46 * Checks the Javadoc of a method or constructor.
47 * </div>
48 *
49 * <p>
50 * Violates parameters and type parameters for which no param tags are present can
51 * be suppressed by defining property {@code allowMissingParamTags}.
52 * </p>
53 *
54 * <p>
55 * Violates methods which return non-void but for which no return tag is present can
56 * be suppressed by defining property {@code allowMissingReturnTag}.
57 * </p>
58 *
59 * <p>
60 * Violates exceptions which are declared to be thrown (by {@code throws} in the method
61 * signature or by {@code throw new} in the method body), but for which no throws tag is
62 * present by activation of property {@code validateThrows}.
63 * Note that {@code throw new} is not checked in the following places:
64 * </p>
65 * <ul>
66 * <li>
67 * Inside a try block (with catch). It is not possible to determine if the thrown
68 * exception can be caught by the catch block as there is no knowledge of the
69 * inheritance hierarchy, so the try block is ignored entirely. However, catch
70 * and finally blocks, as well as try blocks without catch, are still checked.
71 * </li>
72 * <li>
73 * Local classes, anonymous classes and lambda expressions. It is not known when the
74 * throw statements inside such classes are going to be evaluated, so they are ignored.
75 * </li>
76 * </ul>
77 *
78 * <p>
79 * ATTENTION: Checkstyle does not have information about hierarchy of exception types
80 * so usage of base class is considered as separate exception type.
81 * As workaround, you need to specify both types in javadoc (parent and exact type).
82 * </p>
83 *
84 * <p>
85 * Javadoc is not required on a method that is tagged with the {@code @Override}
86 * annotation. However, under Java 5 it is not possible to mark a method required
87 * for an interface (this was <i>corrected</i> under Java 6). Hence, Checkstyle
88 * supports using the convention of using a single {@code {@inheritDoc}} tag
89 * instead of all the other tags.
90 * </p>
91 *
92 * <p>
93 * Note that only inheritable items will allow the {@code {@inheritDoc}}
94 * tag to be used in place of comments. Static methods at all visibilities,
95 * private non-static methods and constructors are not inheritable.
96 * </p>
97 *
98 * <p>
99 * For example, if the following method is implementing a method required by
100 * an interface, then the Javadoc could be done as:
101 * </p>
102 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
103 * /** {@inheritDoc} */
104 * public int checkReturnTag(final int aTagIndex,
105 * JavadocTag[] aTags,
106 * int aLineNo)
107 * </code></pre></div>
108 *
109 * @since 3.0
110 */
111 @FileStatefulCheck
112 public class JavadocMethodCheck extends AbstractJavadocCheck {
113
114 /**
115 * A key is pointing to the warning message text in "messages.properties"
116 * file.
117 */
118 public static final String MSG_CLASS_INFO = "javadoc.classInfo";
119
120 /**
121 * A key is pointing to the warning message text in "messages.properties"
122 * file.
123 */
124 public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral";
125
126 /**
127 * A key is pointing to the warning message text in "messages.properties"
128 * file.
129 */
130 public static final String MSG_INVALID_INHERIT_DOC = "javadoc.invalidInheritDoc";
131
132 /**
133 * A key is pointing to the warning message text in "messages.properties"
134 * file.
135 */
136 public static final String MSG_UNUSED_TAG = "javadoc.unusedTag";
137
138 /**
139 * A key is pointing to the warning message text in "messages.properties"
140 * file.
141 */
142 public static final String MSG_EXPECTED_TAG = "javadoc.expectedTag";
143
144 /**
145 * A key is pointing to the warning message text in "messages.properties"
146 * file.
147 */
148 public static final String MSG_RETURN_EXPECTED = "javadoc.return.expected";
149
150 /**
151 * A key is pointing to the warning message text in "messages.properties"
152 * file.
153 */
154 public static final String MSG_DUPLICATE_TAG = "javadoc.duplicateTag";
155
156 /** Html element start symbol. */
157 private static final String ELEMENT_START = "<";
158
159 /** Html element end symbol. */
160 private static final String ELEMENT_END = ">";
161
162 /** Javadoc tags collected from the current Javadoc tree. */
163 private final List<JavadocTag> javadocTags = new ArrayList<>();
164
165 /**
166 * Control whether to allow inline return tags.
167 */
168 private boolean allowInlineReturn;
169
170 /** Specify the access modifiers where Javadoc comments are checked. */
171 private AccessModifierOption[] accessModifiers = {
172 AccessModifierOption.PUBLIC,
173 AccessModifierOption.PROTECTED,
174 AccessModifierOption.PACKAGE,
175 AccessModifierOption.PRIVATE,
176 };
177
178 /**
179 * Control whether to validate {@code throws} tags.
180 */
181 private boolean validateThrows;
182
183 /**
184 * Control whether to ignore violations when a method has parameters but does
185 * not have matching {@code param} tags in the javadoc.
186 */
187 private boolean allowMissingParamTags;
188
189 /**
190 * Control whether to ignore violations when a method returns non-void type
191 * and does not have a {@code return} tag in the javadoc.
192 */
193 private boolean allowMissingReturnTag;
194
195 /** Specify annotations that allow missed documentation. */
196 private Set<String> allowedAnnotations = Set.of("Override");
197
198 /** Java AST node whose attached Javadoc is currently being processed. */
199 private DetailAST currentAst;
200
201 /**
202 * Setter to control whether to allow inline return tags.
203 *
204 * @param value a {@code boolean} value
205 * @since 10.23.0
206 */
207 public void setAllowInlineReturn(boolean value) {
208 allowInlineReturn = value;
209 }
210
211 /**
212 * Setter to control whether to validate {@code throws} tags.
213 *
214 * @param value user's value.
215 * @since 6.0
216 */
217 public void setValidateThrows(boolean value) {
218 validateThrows = value;
219 }
220
221 /**
222 * Setter to specify annotations that allow missed documentation.
223 *
224 * @param userAnnotations user's value.
225 * @since 6.0
226 */
227 public void setAllowedAnnotations(String... userAnnotations) {
228 allowedAnnotations = Set.of(userAnnotations);
229 }
230
231 /**
232 * Setter to specify the access modifiers where Javadoc comments are checked.
233 *
234 * @param accessModifiers access modifiers.
235 * @since 8.42
236 */
237 public void setAccessModifiers(AccessModifierOption... accessModifiers) {
238 this.accessModifiers =
239 UnmodifiableCollectionUtil.copyOfArray(accessModifiers, accessModifiers.length);
240 }
241
242 /**
243 * Setter to control whether to ignore violations when a method has parameters
244 * but does not have matching {@code param} tags in the javadoc.
245 *
246 * @param flag a {@code Boolean} value
247 * @since 3.1
248 */
249 public void setAllowMissingParamTags(boolean flag) {
250 allowMissingParamTags = flag;
251 }
252
253 /**
254 * Setter to control whether to ignore violations when a method returns non-void type
255 * and does not have a {@code return} tag in the javadoc.
256 *
257 * @param flag a {@code Boolean} value
258 * @since 3.1
259 */
260 public void setAllowMissingReturnTag(boolean flag) {
261 allowMissingReturnTag = flag;
262 }
263
264 /**
265 * Setter to control when to print violations if the Javadoc being examined by this check
266 * violates the tight html rules defined at
267 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
268 * Tight-HTML Rules</a>.
269 *
270 * @param shouldReportViolation value to which the field shall be set to
271 * @since 8.3
272 * @propertySince 13.7.0
273 */
274 @Override
275 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
276 super.setViolateExecutionOnNonTightHtml(shouldReportViolation);
277 }
278
279 @Override
280 public final int[] getRequiredTokens() {
281 return CommonUtil.EMPTY_INT_ARRAY;
282 }
283
284 @Override
285 public int[] getDefaultTokens() {
286 return getAcceptableTokens();
287 }
288
289 @Override
290 public int[] getAcceptableTokens() {
291 return new int[] {
292 TokenTypes.METHOD_DEF,
293 TokenTypes.CTOR_DEF,
294 TokenTypes.ANNOTATION_FIELD_DEF,
295 TokenTypes.COMPACT_CTOR_DEF,
296 };
297 }
298
299 @Override
300 public final void visitToken(DetailAST ast) {
301 if (shouldCheck(ast)) {
302 final DetailAST blockCommentNode = JavadocUtil.getAttachedJavadocComment(ast);
303 if (blockCommentNode != null) {
304 currentAst = ast;
305 super.visitToken(blockCommentNode);
306 }
307 }
308 }
309
310 @Override
311 public void beginJavadocTree(DetailNode rootAst) {
312 javadocTags.clear();
313 }
314
315 @Override
316 public void finishJavadocTree(DetailNode rootAst) {
317 checkCollectedTags();
318 }
319
320 @Override
321 public int[] getDefaultJavadocTokens() {
322 return getRequiredJavadocTokens();
323 }
324
325 @Override
326 public int[] getRequiredJavadocTokens() {
327 return new int[] {
328 JavadocCommentsTokenTypes.PARAM_BLOCK_TAG,
329 JavadocCommentsTokenTypes.RETURN_BLOCK_TAG,
330 JavadocCommentsTokenTypes.RETURN_INLINE_TAG,
331 JavadocCommentsTokenTypes.THROWS_BLOCK_TAG,
332 JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG,
333 JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG,
334 };
335 }
336
337 @Override
338 public void visitJavadocToken(DetailNode ast) {
339 switch (ast.getType()) {
340 case JavadocCommentsTokenTypes.RETURN_BLOCK_TAG -> collectReturn(ast);
341 case JavadocCommentsTokenTypes.RETURN_INLINE_TAG -> {
342 if (allowInlineReturn) {
343 collectReturn(ast);
344 }
345 }
346 case JavadocCommentsTokenTypes.INHERIT_DOC_INLINE_TAG -> collectInheritDoc();
347 case JavadocCommentsTokenTypes.PARAM_BLOCK_TAG -> collectParam(ast);
348 case JavadocCommentsTokenTypes.THROWS_BLOCK_TAG -> collectThrows(ast, "throws");
349 case JavadocCommentsTokenTypes.EXCEPTION_BLOCK_TAG -> collectThrows(ast, "exception");
350 default -> throw new IllegalArgumentException("Unknown javadoc token type " + ast);
351 }
352 }
353
354 /**
355 * Collects a return tag if it has a description.
356 *
357 * @param ast the return tag node
358 */
359 private void collectReturn(DetailNode ast) {
360 if (JavadocUtil.findFirstToken(ast, JavadocCommentsTokenTypes.DESCRIPTION) != null) {
361 javadocTags.add(new JavadocTag(ast.getLineNumber(), ast.getColumnNumber(), "return"));
362 }
363 }
364
365 /**
366 * Collects an inheritDoc tag.
367 */
368 private void collectInheritDoc() {
369 javadocTags.add(new JavadocTag(0, 0, "inheritDoc"));
370 }
371
372 /**
373 * Collects a param tag.
374 *
375 * @param ast the param tag node
376 */
377 private void collectParam(DetailNode ast) {
378 final DetailNode parameterName = JavadocUtil.findFirstToken(
379 ast, JavadocCommentsTokenTypes.PARAMETER_NAME);
380 if (parameterName != null) {
381 javadocTags.add(new JavadocTag(ast.getLineNumber(), ast.getColumnNumber(),
382 "param", parameterName.getText()));
383 }
384 }
385
386 /**
387 * Collects a throws or exception tag.
388 *
389 * @param ast the throws or exception tag node
390 * @param tagName the tag name
391 */
392 private void collectThrows(DetailNode ast, String tagName) {
393 final DetailNode identifier = JavadocUtil.findFirstToken(
394 ast, JavadocCommentsTokenTypes.IDENTIFIER);
395 if (identifier != null) {
396 javadocTags.add(new JavadocTag(0, 0,
397 tagName, identifier.getText()));
398 }
399 }
400
401 /**
402 * Checks collected Javadoc tags against the current AST node.
403 */
404 private void checkCollectedTags() {
405 final List<JavadocTag> tagsToCheck = new ArrayList<>(javadocTags);
406 if (!hasShortCircuitTag(currentAst, tagsToCheck)) {
407 if (currentAst.getType() == TokenTypes.ANNOTATION_FIELD_DEF) {
408 checkReturnTag(tagsToCheck, currentAst.getLineNo(), true);
409 }
410 else {
411 boolean hasInheritDocTag = false;
412 final Iterator<JavadocTag> iterator = tagsToCheck.iterator();
413 while (!hasInheritDocTag && iterator.hasNext()) {
414 hasInheritDocTag = iterator.next().isInheritDocTag();
415 }
416 final boolean reportExpectedTags = !hasInheritDocTag
417 && !AnnotationUtil.containsAnnotation(currentAst, allowedAnnotations);
418 if (currentAst.getType() == TokenTypes.COMPACT_CTOR_DEF) {
419 checkRecordParamTags(tagsToCheck, currentAst, reportExpectedTags);
420 }
421 else {
422 checkParamTags(tagsToCheck, currentAst, reportExpectedTags);
423 }
424 final List<ExceptionInfo> thrown =
425 combineExceptionInfo(getThrows(currentAst), getThrowed(currentAst));
426 checkThrowsTags(tagsToCheck, thrown, reportExpectedTags);
427 if (CheckUtil.isNonVoidMethod(currentAst)) {
428 checkReturnTag(tagsToCheck, currentAst.getLineNo(), reportExpectedTags);
429 }
430 }
431 }
432 tagsToCheck.stream()
433 .filter(javadocTag -> !javadocTag.isInheritDocTag())
434 .forEach(javadocTag -> log(javadocTag.getLineNo(), MSG_UNUSED_TAG_GENERAL));
435 }
436
437 /**
438 * Checks whether the given declaration should be validated.
439 *
440 * <p>The declaration is checked only when both its own access modifier and the
441 * access modifier of the surrounding type match the configured
442 * {@code accessModifiers}.</p>
443 *
444 * @param ast the method, constructor, annotation field, or compact constructor
445 * AST node to check
446 * @return {@code true} if the declaration is inside the configured access scope
447 */
448 private boolean shouldCheck(final DetailAST ast) {
449 final Optional<AccessModifierOption> surroundingAccessModifier = CheckUtil
450 .getSurroundingAccessModifier(ast);
451 final AccessModifierOption accessModifier = CheckUtil
452 .getAccessModifierFromModifiersToken(ast);
453 return surroundingAccessModifier.isPresent() && Arrays.stream(accessModifiers)
454 .anyMatch(modifier -> modifier == surroundingAccessModifier.get())
455 && Arrays.stream(accessModifiers).anyMatch(modifier -> modifier == accessModifier);
456 }
457
458 /**
459 * Retrieves the list of record components from a given record definition.
460 *
461 * @param recordDef the AST node representing the record definition
462 * @return a list of AST nodes representing the record components
463 */
464 private static List<DetailAST> getRecordComponents(final DetailAST recordDef) {
465 final List<DetailAST> components = new ArrayList<>();
466 final DetailAST recordDecl = recordDef.findFirstToken(TokenTypes.RECORD_COMPONENTS);
467
468 DetailAST child = recordDecl.getFirstChild();
469 while (child != null) {
470 if (child.getType() == TokenTypes.RECORD_COMPONENT_DEF) {
471 components.add(child.findFirstToken(TokenTypes.IDENT));
472 }
473 child = child.getNextSibling();
474 }
475 return components;
476 }
477
478 /**
479 * Finds the nearest ancestor record definition node for the given AST node.
480 *
481 * @param ast the AST node to start searching from
482 * @return the nearest {@code RECORD_DEF} AST node, or {@code null} if not found
483 */
484 private static DetailAST getRecordDef(DetailAST ast) {
485 DetailAST current = ast;
486 while (current.getType() != TokenTypes.RECORD_DEF) {
487 current = current.getParent();
488 }
489 return current;
490 }
491
492 /**
493 * Validates whether the Javadoc has a short circuit tag. Currently, this is
494 * the inheritTag. Any violations are logged.
495 *
496 * @param ast the construct being checked
497 * @param tags the list of Javadoc tags associated with the construct
498 * @return true if the construct has a short circuit tag.
499 */
500 private boolean hasShortCircuitTag(final DetailAST ast, final List<JavadocTag> tags) {
501 boolean result = true;
502 // Check if it contains {@inheritDoc} tag
503 if (tags.size() == 1
504 && tags.getFirst().isInheritDocTag()) {
505 // Invalid if private, a constructor, or a static method
506 if (!JavadocTagInfo.INHERIT_DOC.isValidOn(ast)) {
507 log(ast, MSG_INVALID_INHERIT_DOC);
508 }
509 }
510 else {
511 result = false;
512 }
513 return result;
514 }
515
516 /**
517 * Computes the parameter nodes for a method.
518 *
519 * @param ast the method node.
520 * @return the list of parameter nodes for ast.
521 */
522 private static List<DetailAST> getParameters(DetailAST ast) {
523 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
524 final List<DetailAST> returnValue = new ArrayList<>();
525
526 DetailAST child = params.getFirstChild();
527 while (child != null) {
528 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
529 if (ident != null) {
530 returnValue.add(ident);
531 }
532 child = child.getNextSibling();
533 }
534 return returnValue;
535 }
536
537 /**
538 * Computes the exception nodes for a method.
539 *
540 * @param ast the method node.
541 * @return the list of exception nodes for ast.
542 */
543 private static List<ExceptionInfo> getThrows(DetailAST ast) {
544 final List<ExceptionInfo> returnValue = new ArrayList<>();
545 final DetailAST throwsAST = ast
546 .findFirstToken(TokenTypes.LITERAL_THROWS);
547 if (throwsAST != null) {
548 DetailAST child = throwsAST.getFirstChild();
549 while (child != null) {
550 if (child.getType() == TokenTypes.IDENT
551 || child.getType() == TokenTypes.DOT) {
552 returnValue.add(getExceptionInfo(child));
553 }
554 child = child.getNextSibling();
555 }
556 }
557 return returnValue;
558 }
559
560 /**
561 * Get ExceptionInfo for all exceptions that throws in method code by 'throw new'.
562 *
563 * @param methodAst method DetailAST object where to find exceptions
564 * @return list of ExceptionInfo
565 */
566 private static List<ExceptionInfo> getThrowed(DetailAST methodAst) {
567 final List<ExceptionInfo> returnValue = new ArrayList<>();
568 final List<DetailAST> throwLiterals = findTokensInAstByType(methodAst,
569 TokenTypes.LITERAL_THROW);
570 for (DetailAST throwAst : throwLiterals) {
571 if (!isInIgnoreBlock(methodAst, throwAst)) {
572 final DetailAST newAst = throwAst.getFirstChild().getFirstChild();
573 if (newAst.getType() == TokenTypes.LITERAL_NEW) {
574 final DetailAST child = newAst.getFirstChild();
575 returnValue.add(getExceptionInfo(child));
576 }
577 }
578 }
579 return returnValue;
580 }
581
582 /**
583 * Get ExceptionInfo instance.
584 *
585 * @param ast DetailAST object where to find exceptions node;
586 * @return ExceptionInfo
587 */
588 private static ExceptionInfo getExceptionInfo(DetailAST ast) {
589 final FullIdent ident = FullIdent.createFullIdent(ast);
590 final DetailAST firstClassNameNode = getFirstClassNameNode(ast);
591 return new ExceptionInfo(firstClassNameNode,
592 new ClassInfo(new Token(ident)));
593 }
594
595 /**
596 * Get node where class name of exception starts.
597 *
598 * @param ast DetailAST object where to find exceptions node;
599 * @return exception node where class name starts
600 */
601 private static DetailAST getFirstClassNameNode(DetailAST ast) {
602 DetailAST startNode = ast;
603 while (startNode.getType() == TokenTypes.DOT) {
604 startNode = startNode.getFirstChild();
605 }
606 return startNode;
607 }
608
609 /**
610 * Checks if a 'throw' usage is contained within a block that should be ignored.
611 * Such blocks consist of try (with catch) blocks, local classes, anonymous classes,
612 * and lambda expressions. Note that a try block without catch is not considered.
613 *
614 * @param methodBodyAst DetailAST node representing the method body
615 * @param throwAst DetailAST node representing the 'throw' literal
616 * @return true if throwAst is inside a block that should be ignored
617 */
618 private static boolean isInIgnoreBlock(DetailAST methodBodyAst, DetailAST throwAst) {
619 DetailAST ancestor = throwAst;
620 while (ancestor != methodBodyAst) {
621 if (ancestor.getType() == TokenTypes.LAMBDA
622 || ancestor.getType() == TokenTypes.OBJBLOCK
623 || ancestor.findFirstToken(TokenTypes.LITERAL_CATCH) != null) {
624 // throw is inside a lambda expression/anonymous class/local class,
625 // or throw is inside a try block, and there is a catch block
626 break;
627 }
628 if (ancestor.getType() == TokenTypes.LITERAL_CATCH
629 || ancestor.getType() == TokenTypes.LITERAL_FINALLY) {
630 // if the throw is inside a catch or finally block,
631 // skip the immediate ancestor (try token)
632 ancestor = ancestor.getParent();
633 }
634 ancestor = ancestor.getParent();
635 }
636 return ancestor != methodBodyAst;
637 }
638
639 /**
640 * Combine ExceptionInfo collections together by matching names.
641 *
642 * @param first the first collection of ExceptionInfo
643 * @param second the second collection of ExceptionInfo
644 * @return combined list of ExceptionInfo
645 */
646 private static List<ExceptionInfo> combineExceptionInfo(Collection<ExceptionInfo> first,
647 Iterable<ExceptionInfo> second) {
648 final List<ExceptionInfo> result = new ArrayList<>(first);
649 for (ExceptionInfo exceptionInfo : second) {
650 if (result.stream().noneMatch(item -> isExceptionInfoSame(item, exceptionInfo))) {
651 result.add(exceptionInfo);
652 }
653 }
654 return result;
655 }
656
657 /**
658 * Finds node of specified type among root children, siblings, siblings children
659 * on any deep level.
660 *
661 * @param root DetailAST
662 * @param astType value of TokenType
663 * @return {@link List} of {@link DetailAST} nodes which matches the predicate.
664 */
665 public static List<DetailAST> findTokensInAstByType(DetailAST root, int astType) {
666 final List<DetailAST> result = new ArrayList<>();
667 // iterative preorder depth-first search
668 DetailAST curNode = root;
669 do {
670 // process curNode
671 if (curNode.getType() == astType) {
672 result.add(curNode);
673 }
674 // process children (if any)
675 if (curNode.hasChildren()) {
676 curNode = curNode.getFirstChild();
677 continue;
678 }
679 // backtrack to parent if last child, stopping at root
680 while (curNode.getNextSibling() == null) {
681 curNode = curNode.getParent();
682 }
683 // explore siblings if not root
684 if (curNode != root) {
685 curNode = curNode.getNextSibling();
686 }
687 } while (curNode != root);
688 return result;
689 }
690
691 /**
692 * Checks if all record components in a compact constructor have
693 * corresponding {@code @param} tags.
694 * Reports missing or extra {@code @param} tags in the Javadoc.
695 *
696 * @param tags the list of Javadoc tags
697 * @param compactDef the compact constructor AST node
698 * @param reportExpectedTags whether to report missing {@code @param} tags
699 */
700 private void checkRecordParamTags(final List<JavadocTag> tags,
701 final DetailAST compactDef, boolean reportExpectedTags) {
702
703 final DetailAST parent = getRecordDef(compactDef);
704 final List<DetailAST> params = getRecordComponents(parent);
705
706 final ListIterator<JavadocTag> tagIt = tags.listIterator();
707 while (tagIt.hasNext()) {
708 final JavadocTag tag = tagIt.next();
709
710 if (!tag.isParamTag()) {
711 continue;
712 }
713
714 tagIt.remove();
715
716 final String arg1 = tag.getFirstArg();
717 final boolean found = removeMatchingParam(params, arg1);
718
719 if (!found) {
720 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG,
721 JavadocTagInfo.PARAM.getText(), arg1);
722 }
723 }
724
725 if (!allowMissingParamTags && reportExpectedTags) {
726 for (DetailAST param : params) {
727 log(compactDef, MSG_EXPECTED_TAG,
728 JavadocTagInfo.PARAM.getText(), param.getText());
729 }
730 }
731 }
732
733 /**
734 * Checks a set of tags for matching parameters.
735 *
736 * @param tags the tags to check
737 * @param parent the node which takes the parameters
738 * @param reportExpectedTags whether we should report if do not find
739 * expected tag
740 */
741 private void checkParamTags(final List<JavadocTag> tags,
742 final DetailAST parent, boolean reportExpectedTags) {
743 final List<DetailAST> params = getParameters(parent);
744 final List<DetailAST> typeParams = CheckUtil
745 .getTypeParameters(parent);
746
747 // Loop over the tags, checking to see they exist in the params.
748 final ListIterator<JavadocTag> tagIt = tags.listIterator();
749 while (tagIt.hasNext()) {
750 final JavadocTag tag = tagIt.next();
751
752 if (!tag.isParamTag()) {
753 continue;
754 }
755
756 tagIt.remove();
757
758 final String arg1 = tag.getFirstArg();
759 boolean found = removeMatchingParam(params, arg1);
760
761 if (arg1.endsWith(ELEMENT_END)) {
762 found = searchMatchingTypeParameter(typeParams,
763 arg1.substring(1, arg1.length() - 1));
764 }
765
766 // Handle extra JavadocTag
767 if (!found) {
768 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG,
769 JavadocTagInfo.PARAM.getText(), arg1);
770 }
771 }
772
773 // Now dump out all type parameters/parameters without tags :- unless
774 // the user has chosen to suppress these problems
775 if (!allowMissingParamTags && reportExpectedTags) {
776 for (DetailAST param : params) {
777 log(param, MSG_EXPECTED_TAG,
778 JavadocTagInfo.PARAM.getText(), param.getText());
779 }
780
781 for (DetailAST typeParam : typeParams) {
782 log(typeParam, MSG_EXPECTED_TAG,
783 JavadocTagInfo.PARAM.getText(),
784 ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText()
785 + ELEMENT_END);
786 }
787 }
788 }
789
790 /**
791 * Returns true if required type found in type parameters.
792 *
793 * @param typeParams
794 * collection of type parameters
795 * @param requiredTypeName
796 * name of required type
797 * @return true if required type found in type parameters.
798 */
799 private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams,
800 String requiredTypeName) {
801 // Loop looking for matching type param
802 final Iterator<DetailAST> typeParamsIt = typeParams.iterator();
803 boolean found = false;
804 while (typeParamsIt.hasNext()) {
805 final DetailAST typeParam = typeParamsIt.next();
806 if (typeParam.findFirstToken(TokenTypes.IDENT).getText()
807 .equals(requiredTypeName)) {
808 found = true;
809 typeParamsIt.remove();
810 break;
811 }
812 }
813 return found;
814 }
815
816 /**
817 * Remove parameter from params collection by name.
818 *
819 * @param params collection of DetailAST parameters
820 * @param paramName name of parameter
821 * @return true if parameter found and removed
822 */
823 private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) {
824 boolean found = false;
825 final Iterator<DetailAST> paramIt = params.iterator();
826 while (paramIt.hasNext()) {
827 final DetailAST param = paramIt.next();
828 if (param.getText().equals(paramName)) {
829 found = true;
830 paramIt.remove();
831 break;
832 }
833 }
834 return found;
835 }
836
837 /**
838 * Checks for only one return tag. All return tags will be removed from the
839 * supplied list.
840 *
841 * @param tags the tags to check
842 * @param lineNo the line number of the expected tag
843 * @param reportExpectedTags whether we should report if do not find
844 * expected tag
845 */
846 private void checkReturnTag(List<JavadocTag> tags, int lineNo,
847 boolean reportExpectedTags) {
848 // Loop over tags finding return tags. After the first one, report a violation
849 boolean found = false;
850 final ListIterator<JavadocTag> it = tags.listIterator();
851 while (it.hasNext()) {
852 final JavadocTag javadocTag = it.next();
853 if (javadocTag.isReturnTag()) {
854 if (found) {
855 log(javadocTag.getLineNo(), javadocTag.getColumnNo(),
856 MSG_DUPLICATE_TAG,
857 JavadocTagInfo.RETURN.getText());
858 }
859 found = true;
860 it.remove();
861 }
862 }
863
864 // Handle there being no @return tags :- unless
865 // the user has chosen to suppress these problems
866 if (!found && !allowMissingReturnTag && reportExpectedTags) {
867 log(lineNo, MSG_RETURN_EXPECTED);
868 }
869 }
870
871 /**
872 * Checks a set of tags for matching throws.
873 *
874 * @param tags the tags to check
875 * @param throwsList the throws to check
876 * @param reportExpectedTags whether we should report if do not find
877 * expected tag
878 */
879 private void checkThrowsTags(List<JavadocTag> tags,
880 List<ExceptionInfo> throwsList, boolean reportExpectedTags) {
881 // Loop over the tags, checking to see they exist in the throws.
882 final ListIterator<JavadocTag> tagIt = tags.listIterator();
883 while (tagIt.hasNext()) {
884 final JavadocTag tag = tagIt.next();
885
886 if (!tag.isThrowsTag()) {
887 continue;
888 }
889 tagIt.remove();
890
891 // Loop looking for matching throw
892 processThrows(throwsList, tag.getFirstArg());
893 }
894 // Now dump out all throws without tags :- unless
895 // the user has chosen to suppress these problems
896 if (validateThrows && reportExpectedTags) {
897 throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound())
898 .forEach(exceptionInfo -> {
899 final Token token = exceptionInfo.getName();
900 log(exceptionInfo.getAst(),
901 MSG_EXPECTED_TAG,
902 JavadocTagInfo.THROWS.getText(), token.text());
903 });
904 }
905 }
906
907 /**
908 * Verifies that documented exception is in throws.
909 *
910 * @param throwsIterable collection of throws
911 * @param documentedClassName documented exception class name
912 */
913 private static void processThrows(Iterable<ExceptionInfo> throwsIterable,
914 String documentedClassName) {
915 for (ExceptionInfo exceptionInfo : throwsIterable) {
916 if (isClassNamesSame(exceptionInfo.getName().text(),
917 documentedClassName)) {
918 exceptionInfo.setFound();
919 break;
920 }
921 }
922 }
923
924 /**
925 * Check that ExceptionInfo objects are same by name.
926 *
927 * @param info1 ExceptionInfo object
928 * @param info2 ExceptionInfo object
929 * @return true is ExceptionInfo object have the same name
930 */
931 private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) {
932 return isClassNamesSame(info1.getName().text(),
933 info2.getName().text());
934 }
935
936 /**
937 * Check that class names are same by short name of class. If some class name is fully
938 * qualified it is cut to short name.
939 *
940 * @param class1 class name
941 * @param class2 class name
942 * @return true is ExceptionInfo object have the same name
943 */
944 private static boolean isClassNamesSame(String class1, String class2) {
945 final String class1ShortName = class1
946 .substring(class1.lastIndexOf('.') + 1);
947 final String class2ShortName = class2
948 .substring(class2.lastIndexOf('.') + 1);
949 return class1ShortName.equals(class2ShortName);
950 }
951
952 /**
953 * Contains class's {@code Token}.
954 *
955 * @param name {@code FullIdent} associated with this class.
956 */
957 private record ClassInfo(Token name) {
958 }
959
960 /**
961 * Represents text element with location in the text.
962 *
963 * @param text Token's text.
964 */
965 private record Token(String text) {
966
967 /**
968 * Converts FullIdent to Token.
969 *
970 * @param fullIdent full ident to convert.
971 */
972 private Token(FullIdent fullIdent) {
973 this(fullIdent.getText());
974 }
975 }
976
977 /** Stores useful information about declared exception. */
978 private static final class ExceptionInfo {
979
980 /** AST node representing this exception. */
981 private final DetailAST ast;
982
983 /** Class information associated with this exception. */
984 private final ClassInfo classInfo;
985 /** Does the exception have throws tag associated with. */
986 private boolean found;
987
988 /**
989 * Creates new instance for {@code FullIdent}.
990 *
991 * @param ast AST node representing this exception
992 * @param classInfo class info
993 */
994 private ExceptionInfo(DetailAST ast, ClassInfo classInfo) {
995 this.ast = ast;
996 this.classInfo = classInfo;
997 }
998
999 /**
1000 * Gets the AST node representing this exception.
1001 *
1002 * @return the AST node representing this exception
1003 */
1004 private DetailAST getAst() {
1005 return ast;
1006 }
1007
1008 /** Mark that the exception has associated throws tag. */
1009 private void setFound() {
1010 found = true;
1011 }
1012
1013 /**
1014 * Checks that the exception has throws tag associated with it.
1015 *
1016 * @return whether the exception has throws tag associated with
1017 */
1018 private boolean isFound() {
1019 return found;
1020 }
1021
1022 /**
1023 * Gets exception name.
1024 *
1025 * @return exception's name
1026 */
1027 private Token getName() {
1028 return classInfo.name();
1029 }
1030
1031 }
1032
1033 }