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 * 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 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 714 while (tagIt.hasNext()) { 715 final JavadocTag tag = tagIt.next(); 716 717 if (!tag.isParamTag()) { 718 continue; 719 } 720 721 tagIt.remove(); 722 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 if (!allowMissingParamTags && reportExpectedTags) { 733 for (DetailAST param : params) { 734 log(compactDef, MSG_EXPECTED_TAG, 735 JavadocTagInfo.PARAM.getText(), param.getText()); 736 } 737 } 738 } 739 740 /** 741 * Checks a set of tags for matching parameters. 742 * 743 * @param tags the tags to check 744 * @param parent the node which takes the parameters 745 * @param reportExpectedTags whether we should report if do not find 746 * expected tag 747 */ 748 private void checkParamTags(final List<JavadocTag> tags, 749 final DetailAST parent, boolean reportExpectedTags) { 750 final List<DetailAST> params = getParameters(parent); 751 final List<DetailAST> typeParams = CheckUtil 752 .getTypeParameters(parent); 753 754 // Loop over the tags, checking to see they exist in the params. 755 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 756 while (tagIt.hasNext()) { 757 final JavadocTag tag = tagIt.next(); 758 759 if (!tag.isParamTag()) { 760 continue; 761 } 762 763 tagIt.remove(); 764 765 final String arg1 = tag.getFirstArg(); 766 boolean found = removeMatchingParam(params, arg1); 767 768 if (arg1.endsWith(ELEMENT_END)) { 769 found = searchMatchingTypeParameter(typeParams, 770 arg1.substring(1, arg1.length() - 1)); 771 } 772 773 // Handle extra JavadocTag 774 if (!found) { 775 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, 776 JavadocTagInfo.PARAM.getText(), arg1); 777 } 778 } 779 780 // Now dump out all type parameters/parameters without tags :- unless 781 // the user has chosen to suppress these problems 782 if (!allowMissingParamTags && reportExpectedTags) { 783 for (DetailAST param : params) { 784 log(param, MSG_EXPECTED_TAG, 785 JavadocTagInfo.PARAM.getText(), param.getText()); 786 } 787 788 for (DetailAST typeParam : typeParams) { 789 log(typeParam, MSG_EXPECTED_TAG, 790 JavadocTagInfo.PARAM.getText(), 791 ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText() 792 + ELEMENT_END); 793 } 794 } 795 } 796 797 /** 798 * Returns true if required type found in type parameters. 799 * 800 * @param typeParams 801 * collection of type parameters 802 * @param requiredTypeName 803 * name of required type 804 * @return true if required type found in type parameters. 805 */ 806 private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams, 807 String requiredTypeName) { 808 // Loop looking for matching type param 809 final Iterator<DetailAST> typeParamsIt = typeParams.iterator(); 810 boolean found = false; 811 while (typeParamsIt.hasNext()) { 812 final DetailAST typeParam = typeParamsIt.next(); 813 if (typeParam.findFirstToken(TokenTypes.IDENT).getText() 814 .equals(requiredTypeName)) { 815 found = true; 816 typeParamsIt.remove(); 817 break; 818 } 819 } 820 return found; 821 } 822 823 /** 824 * Remove parameter from params collection by name. 825 * 826 * @param params collection of DetailAST parameters 827 * @param paramName name of parameter 828 * @return true if parameter found and removed 829 */ 830 private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) { 831 boolean found = false; 832 final Iterator<DetailAST> paramIt = params.iterator(); 833 while (paramIt.hasNext()) { 834 final DetailAST param = paramIt.next(); 835 if (param.getText().equals(paramName)) { 836 found = true; 837 paramIt.remove(); 838 break; 839 } 840 } 841 return found; 842 } 843 844 /** 845 * Checks for only one return tag. All return tags will be removed from the 846 * supplied list. 847 * 848 * @param tags the tags to check 849 * @param lineNo the line number of the expected tag 850 * @param reportExpectedTags whether we should report if do not find 851 * expected tag 852 */ 853 private void checkReturnTag(List<JavadocTag> tags, int lineNo, 854 boolean reportExpectedTags) { 855 // Loop over tags finding return tags. After the first one, report a violation 856 boolean found = false; 857 final ListIterator<JavadocTag> it = tags.listIterator(); 858 while (it.hasNext()) { 859 final JavadocTag javadocTag = it.next(); 860 if (javadocTag.isReturnTag()) { 861 if (found) { 862 log(javadocTag.getLineNo(), javadocTag.getColumnNo(), 863 MSG_DUPLICATE_TAG, 864 JavadocTagInfo.RETURN.getText()); 865 } 866 found = true; 867 it.remove(); 868 } 869 } 870 871 // Handle there being no @return tags :- unless 872 // the user has chosen to suppress these problems 873 if (!found && !allowMissingReturnTag && reportExpectedTags) { 874 log(lineNo, MSG_RETURN_EXPECTED); 875 } 876 } 877 878 /** 879 * Checks a set of tags for matching throws. 880 * 881 * @param tags the tags to check 882 * @param throwsList the throws to check 883 * @param reportExpectedTags whether we should report if do not find 884 * expected tag 885 */ 886 private void checkThrowsTags(List<JavadocTag> tags, 887 List<ExceptionInfo> throwsList, boolean reportExpectedTags) { 888 // Loop over the tags, checking to see they exist in the throws. 889 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 890 while (tagIt.hasNext()) { 891 final JavadocTag tag = tagIt.next(); 892 893 if (!tag.isThrowsTag()) { 894 continue; 895 } 896 tagIt.remove(); 897 898 // Loop looking for matching throw 899 processThrows(throwsList, tag.getFirstArg()); 900 } 901 // Now dump out all throws without tags :- unless 902 // the user has chosen to suppress these problems 903 if (validateThrows && reportExpectedTags) { 904 throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound()) 905 .forEach(exceptionInfo -> { 906 final Token token = exceptionInfo.getName(); 907 log(exceptionInfo.getAst(), 908 MSG_EXPECTED_TAG, 909 JavadocTagInfo.THROWS.getText(), token.text()); 910 }); 911 } 912 } 913 914 /** 915 * Verifies that documented exception is in throws. 916 * 917 * @param throwsIterable collection of throws 918 * @param documentedClassName documented exception class name 919 */ 920 private static void processThrows(Iterable<ExceptionInfo> throwsIterable, 921 String documentedClassName) { 922 for (ExceptionInfo exceptionInfo : throwsIterable) { 923 if (isClassNamesSame(exceptionInfo.getName().text(), 924 documentedClassName)) { 925 exceptionInfo.setFound(); 926 break; 927 } 928 } 929 } 930 931 /** 932 * Check that ExceptionInfo objects are same by name. 933 * 934 * @param info1 ExceptionInfo object 935 * @param info2 ExceptionInfo object 936 * @return true is ExceptionInfo object have the same name 937 */ 938 private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) { 939 return isClassNamesSame(info1.getName().text(), 940 info2.getName().text()); 941 } 942 943 /** 944 * Check that class names are same by short name of class. If some class name is fully 945 * qualified it is cut to short name. 946 * 947 * @param class1 class name 948 * @param class2 class name 949 * @return true is ExceptionInfo object have the same name 950 */ 951 private static boolean isClassNamesSame(String class1, String class2) { 952 final String class1ShortName = class1 953 .substring(class1.lastIndexOf('.') + 1); 954 final String class2ShortName = class2 955 .substring(class2.lastIndexOf('.') + 1); 956 return class1ShortName.equals(class2ShortName); 957 } 958 959 /** 960 * Contains class's {@code Token}. 961 * 962 * @param name {@code FullIdent} associated with this class. 963 */ 964 private record ClassInfo(Token name) { 965 } 966 967 /** 968 * Represents text element with location in the text. 969 * 970 * @param text Token's text. 971 */ 972 private record Token(String text) { 973 974 /** 975 * Converts FullIdent to Token. 976 * 977 * @param fullIdent full ident to convert. 978 */ 979 private Token(FullIdent fullIdent) { 980 this(fullIdent.getText()); 981 } 982 } 983 984 /** Stores useful information about declared exception. */ 985 private static final class ExceptionInfo { 986 987 /** AST node representing this exception. */ 988 private final DetailAST ast; 989 990 /** Class information associated with this exception. */ 991 private final ClassInfo classInfo; 992 /** Does the exception have throws tag associated with. */ 993 private boolean found; 994 995 /** 996 * Creates new instance for {@code FullIdent}. 997 * 998 * @param ast AST node representing this exception 999 * @param classInfo class info 1000 */ 1001 private ExceptionInfo(DetailAST ast, ClassInfo classInfo) { 1002 this.ast = ast; 1003 this.classInfo = classInfo; 1004 } 1005 1006 /** 1007 * Gets the AST node representing this exception. 1008 * 1009 * @return the AST node representing this exception 1010 */ 1011 private DetailAST getAst() { 1012 return ast; 1013 } 1014 1015 /** Mark that the exception has associated throws tag. */ 1016 private void setFound() { 1017 found = true; 1018 } 1019 1020 /** 1021 * Checks that the exception has throws tag associated with it. 1022 * 1023 * @return whether the exception has throws tag associated with 1024 */ 1025 private boolean isFound() { 1026 return found; 1027 } 1028 1029 /** 1030 * Gets exception name. 1031 * 1032 * @return exception's name 1033 */ 1034 private Token getName() { 1035 return classInfo.name(); 1036 } 1037 1038 } 1039 1040}