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