001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.javadoc; 021 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.Iterator; 026import java.util.List; 027import java.util.ListIterator; 028import java.util.Optional; 029import java.util.Set; 030 031import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 032import com.puppycrawl.tools.checkstyle.api.DetailAST; 033import com.puppycrawl.tools.checkstyle.api.DetailNode; 034import com.puppycrawl.tools.checkstyle.api.FullIdent; 035import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 036import com.puppycrawl.tools.checkstyle.api.TokenTypes; 037import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 038import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 039import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 040import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 041import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 042import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil; 043 044/** 045 * <div> 046 * Checks the Javadoc of a method or constructor. 047 * </div> 048 * 049 * <p> 050 * Violates parameters and type parameters for which no param tags are present can 051 * be suppressed by defining property {@code allowMissingParamTags}. 052 * </p> 053 * 054 * <p> 055 * Violates methods which return non-void but for which no return tag is present can 056 * be suppressed by defining property {@code allowMissingReturnTag}. 057 * </p> 058 * 059 * <p> 060 * Violates exceptions which are declared to be thrown (by {@code throws} in the method 061 * signature or by {@code throw new} in the method body), but for which no throws tag is 062 * present by activation of property {@code validateThrows}. 063 * Note that {@code throw new} is not checked in the following places: 064 * </p> 065 * <ul> 066 * <li> 067 * Inside a try block (with catch). It is not possible to determine if the thrown 068 * exception can be caught by the catch block as there is no knowledge of the 069 * inheritance hierarchy, so the try block is ignored entirely. However, catch 070 * and finally blocks, as well as try blocks without catch, are still checked. 071 * </li> 072 * <li> 073 * Local classes, anonymous classes and lambda expressions. It is not known when the 074 * throw statements inside such classes are going to be evaluated, so they are ignored. 075 * </li> 076 * </ul> 077 * 078 * <p> 079 * ATTENTION: Checkstyle does not have information about hierarchy of exception types 080 * so usage of base class is considered as separate exception type. 081 * As workaround, you need to specify both types in javadoc (parent and exact type). 082 * </p> 083 * 084 * <p> 085 * Javadoc is not required on a method that is tagged with the {@code @Override} 086 * annotation. However, under Java 5 it is not possible to mark a method required 087 * for an interface (this was <i>corrected</i> under Java 6). Hence, Checkstyle 088 * supports using the convention of using a single {@code {@inheritDoc}} tag 089 * instead of all the other tags. 090 * </p> 091 * 092 * <p> 093 * Note that only inheritable items will allow the {@code {@inheritDoc}} 094 * tag to be used in place of comments. Static methods at all visibilities, 095 * private non-static methods and constructors are not inheritable. 096 * </p> 097 * 098 * <p> 099 * 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 112public 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}