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 * {@snippet lang="text" : 103 * /** {@inheritDoc} * / 104 * public int checkReturnTag(final int aTagIndex, 105 * JavadocTag[] aTags, 106 * int aLineNo) 107 * } 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" file. 116 */ 117 public static final String MSG_CLASS_INFO = "javadoc.classInfo"; 118 119 /** 120 * A key is pointing to the warning message text in "messages.properties" file. 121 */ 122 public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral"; 123 124 /** 125 * A key is pointing to the warning message text in "messages.properties" file. 126 */ 127 public static final String MSG_INVALID_INHERIT_DOC = "javadoc.invalidInheritDoc"; 128 129 /** 130 * A key is pointing to the warning message text in "messages.properties" file. 131 */ 132 public static final String MSG_UNUSED_TAG = "javadoc.unusedTag"; 133 134 /** 135 * A key is pointing to the warning message text in "messages.properties" file. 136 */ 137 public static final String MSG_EXPECTED_TAG = "javadoc.expectedTag"; 138 139 /** 140 * A key is pointing to the warning message text in "messages.properties" file. 141 */ 142 public static final String MSG_RETURN_EXPECTED = "javadoc.return.expected"; 143 144 /** 145 * A key is pointing to the warning message text in "messages.properties" file. 146 */ 147 public static final String MSG_DUPLICATE_TAG = "javadoc.duplicateTag"; 148 149 /** Html element start symbol. */ 150 private static final String ELEMENT_START = "<"; 151 152 /** Html element end symbol. */ 153 private static final String ELEMENT_END = ">"; 154 155 /** Javadoc tags collected from the current Javadoc tree. */ 156 private final List<JavadocTag> javadocTags = new ArrayList<>(); 157 158 /** 159 * Control whether to allow inline return tags. 160 */ 161 private boolean allowInlineReturn; 162 163 /** Specify the access modifiers where Javadoc comments are checked. */ 164 private AccessModifierOption[] accessModifiers = { 165 AccessModifierOption.PUBLIC, 166 AccessModifierOption.PROTECTED, 167 AccessModifierOption.PACKAGE, 168 AccessModifierOption.PRIVATE, 169 }; 170 171 /** 172 * Control whether to validate {@code throws} tags. 173 */ 174 private boolean validateThrows; 175 176 /** 177 * Control whether to ignore violations when a method has parameters but does 178 * not have matching {@code param} tags in the javadoc. 179 */ 180 private boolean allowMissingParamTags; 181 182 /** 183 * Control whether to ignore violations when a method returns non-void type 184 * and does not have a {@code return} tag in the javadoc. 185 */ 186 private boolean allowMissingReturnTag; 187 188 /** Specify annotations that allow missed documentation. */ 189 private Set<String> allowedAnnotations = Set.of("Override"); 190 191 /** Java AST node whose attached Javadoc is currently being processed. */ 192 private DetailAST currentAst; 193 194 /** 195 * Creates a new {@code JavadocMethodCheck} instance. 196 */ 197 public JavadocMethodCheck() { 198 // no code by default 199 } 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/writing-javadoc-checks.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 for (JavadocTag tag : tags) { 707 if (!tag.isParamTag()) { 708 continue; 709 } 710 711 if (isDuplicateParamTag(tags, tag)) { 712 log(tag.getLineNo(), tag.getColumnNo(), MSG_DUPLICATE_TAG, 713 JavadocTagInfo.PARAM.getText()); 714 } 715 else { 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 tags.removeIf(JavadocTag::isParamTag); 726 727 if (!allowMissingParamTags && reportExpectedTags) { 728 for (DetailAST param : params) { 729 log(compactDef, MSG_EXPECTED_TAG, 730 JavadocTagInfo.PARAM.getText(), param.getText()); 731 } 732 } 733 } 734 735 /** 736 * Checks a set of tags for matching parameters. 737 * 738 * @param tags the tags to check 739 * @param parent the node which takes the parameters 740 * @param reportExpectedTags whether we should report if do not find expected tag 741 */ 742 private void checkParamTags(final List<JavadocTag> tags, 743 final DetailAST parent, boolean reportExpectedTags) { 744 final List<DetailAST> params = getParameters(parent); 745 final List<DetailAST> typeParams = CheckUtil 746 .getTypeParameters(parent); 747 748 // Loop over the tags, checking to see they exist in the params. 749 for (JavadocTag tag : tags) { 750 if (!tag.isParamTag()) { 751 continue; 752 } 753 754 if (isDuplicateParamTag(tags, tag)) { 755 log(tag.getLineNo(), tag.getColumnNo(), MSG_DUPLICATE_TAG, 756 JavadocTagInfo.PARAM.getText()); 757 } 758 else { 759 final String arg1 = tag.getFirstArg(); 760 boolean found = removeMatchingParam(params, arg1); 761 762 if (arg1.endsWith(ELEMENT_END)) { 763 found = searchMatchingTypeParameter(typeParams, 764 arg1.substring(1, arg1.length() - 1)); 765 } 766 767 // Handle extra JavadocTag 768 if (!found) { 769 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, 770 JavadocTagInfo.PARAM.getText(), arg1); 771 } 772 } 773 } 774 tags.removeIf(JavadocTag::isParamTag); 775 776 // Now dump out all type parameters/parameters without tags :- unless 777 // the user has chosen to suppress these problems 778 if (!allowMissingParamTags && reportExpectedTags) { 779 for (DetailAST param : params) { 780 log(param, MSG_EXPECTED_TAG, 781 JavadocTagInfo.PARAM.getText(), param.getText()); 782 } 783 784 for (DetailAST typeParam : typeParams) { 785 log(typeParam, MSG_EXPECTED_TAG, 786 JavadocTagInfo.PARAM.getText(), 787 ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText() 788 + ELEMENT_END); 789 } 790 } 791 } 792 793 /** 794 * Checks if the {@code @param} tag duplicates an earlier {@code @param} tag. 795 * 796 * @param tags all tags to check 797 * @param tag the tag to check 798 * @return true if there is an earlier {@code @param} tag with the same argument 799 */ 800 private static boolean isDuplicateParamTag(List<JavadocTag> tags, JavadocTag tag) { 801 final int tagIndex = tags.indexOf(tag); 802 boolean result = false; 803 for (int tagPosition = 0; tagPosition < tagIndex; tagPosition++) { 804 final JavadocTag currentTag = tags.get(tagPosition); 805 if (currentTag.isParamTag() 806 && currentTag.getFirstArg().equals(tag.getFirstArg())) { 807 result = true; 808 } 809 } 810 return result; 811 } 812 813 /** 814 * Returns true if required type found in type parameters. 815 * 816 * @param typeParams 817 * collection of type parameters 818 * @param requiredTypeName 819 * name of required type 820 * @return true if required type found in type parameters. 821 */ 822 private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams, 823 String requiredTypeName) { 824 // Loop looking for matching type param 825 final Iterator<DetailAST> typeParamsIt = typeParams.iterator(); 826 boolean found = false; 827 while (typeParamsIt.hasNext()) { 828 final DetailAST typeParam = typeParamsIt.next(); 829 if (typeParam.findFirstToken(TokenTypes.IDENT).getText() 830 .equals(requiredTypeName)) { 831 found = true; 832 typeParamsIt.remove(); 833 break; 834 } 835 } 836 return found; 837 } 838 839 /** 840 * Remove parameter from params collection by name. 841 * 842 * @param params collection of DetailAST parameters 843 * @param paramName name of parameter 844 * @return true if parameter found and removed 845 */ 846 private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) { 847 boolean found = false; 848 final Iterator<DetailAST> paramIt = params.iterator(); 849 while (paramIt.hasNext()) { 850 final DetailAST param = paramIt.next(); 851 if (param.getText().equals(paramName)) { 852 found = true; 853 paramIt.remove(); 854 break; 855 } 856 } 857 return found; 858 } 859 860 /** 861 * Checks for only one return tag. All return tags will be removed from the supplied list. 862 * 863 * @param tags the tags to check 864 * @param lineNo the line number of the expected tag 865 * @param reportExpectedTags whether we should report if do not find expected tag 866 */ 867 private void checkReturnTag(List<JavadocTag> tags, int lineNo, 868 boolean reportExpectedTags) { 869 // Loop over tags finding return tags. After the first one, report a violation 870 boolean found = false; 871 final ListIterator<JavadocTag> it = tags.listIterator(); 872 while (it.hasNext()) { 873 final JavadocTag javadocTag = it.next(); 874 if (javadocTag.isReturnTag()) { 875 if (found) { 876 log(javadocTag.getLineNo(), javadocTag.getColumnNo(), 877 MSG_DUPLICATE_TAG, 878 JavadocTagInfo.RETURN.getText()); 879 } 880 found = true; 881 it.remove(); 882 } 883 } 884 885 // Handle there being no @return tags :- unless 886 // the user has chosen to suppress these problems 887 if (!found && !allowMissingReturnTag && reportExpectedTags) { 888 log(lineNo, MSG_RETURN_EXPECTED); 889 } 890 } 891 892 /** 893 * Checks a set of tags for matching throws. 894 * 895 * @param tags the tags to check 896 * @param throwsList the throws to check 897 * @param reportExpectedTags whether we should report if do not find expected tag 898 */ 899 private void checkThrowsTags(List<JavadocTag> tags, 900 List<ExceptionInfo> throwsList, boolean reportExpectedTags) { 901 // Loop over the tags, checking to see they exist in the throws. 902 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 903 while (tagIt.hasNext()) { 904 final JavadocTag tag = tagIt.next(); 905 906 if (!tag.isThrowsTag()) { 907 continue; 908 } 909 tagIt.remove(); 910 911 // Loop looking for matching throw 912 processThrows(throwsList, tag.getFirstArg()); 913 } 914 // Now dump out all throws without tags :- unless 915 // the user has chosen to suppress these problems 916 if (validateThrows && reportExpectedTags) { 917 throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound()) 918 .forEach(exceptionInfo -> { 919 final Token token = exceptionInfo.getName(); 920 log(exceptionInfo.getAst(), 921 MSG_EXPECTED_TAG, 922 JavadocTagInfo.THROWS.getText(), token.text()); 923 }); 924 } 925 } 926 927 /** 928 * Verifies that documented exception is in throws. 929 * 930 * @param throwsIterable collection of throws 931 * @param documentedClassName documented exception class name 932 */ 933 private static void processThrows(Iterable<ExceptionInfo> throwsIterable, 934 String documentedClassName) { 935 for (ExceptionInfo exceptionInfo : throwsIterable) { 936 if (isClassNamesSame(exceptionInfo.getName().text(), 937 documentedClassName)) { 938 exceptionInfo.setFound(); 939 break; 940 } 941 } 942 } 943 944 /** 945 * Check that ExceptionInfo objects are same by name. 946 * 947 * @param info1 ExceptionInfo object 948 * @param info2 ExceptionInfo object 949 * @return true is ExceptionInfo object have the same name 950 */ 951 private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) { 952 return isClassNamesSame(info1.getName().text(), 953 info2.getName().text()); 954 } 955 956 /** 957 * Check that class names are same by short name of class. If some class name is fully 958 * qualified it is cut to short name. 959 * 960 * @param class1 class name 961 * @param class2 class name 962 * @return true is ExceptionInfo object have the same name 963 */ 964 private static boolean isClassNamesSame(String class1, String class2) { 965 final String class1ShortName = class1 966 .substring(class1.lastIndexOf('.') + 1); 967 final String class2ShortName = class2 968 .substring(class2.lastIndexOf('.') + 1); 969 return class1ShortName.equals(class2ShortName); 970 } 971 972 /** 973 * Contains class's {@code Token}. 974 * 975 * @param name {@code FullIdent} associated with this class. 976 */ 977 private record ClassInfo(Token name) { 978 } 979 980 /** 981 * Represents text element with location in the text. 982 * 983 * @param text Token's text. 984 */ 985 private record Token(String text) { 986 987 /** 988 * Converts FullIdent to Token. 989 * 990 * @param fullIdent full ident to convert. 991 */ 992 private Token(FullIdent fullIdent) { 993 this(fullIdent.getText()); 994 } 995 } 996 997 /** Stores useful information about declared exception. */ 998 private static final class ExceptionInfo { 999 1000 /** AST node representing this exception. */ 1001 private final DetailAST ast; 1002 1003 /** Class information associated with this exception. */ 1004 private final ClassInfo classInfo; 1005 /** Does the exception have throws tag associated with. */ 1006 private boolean found; 1007 1008 /** 1009 * Creates new instance for {@code FullIdent}. 1010 * 1011 * @param ast AST node representing this exception 1012 * @param classInfo class info 1013 */ 1014 private ExceptionInfo(DetailAST ast, ClassInfo classInfo) { 1015 this.ast = ast; 1016 this.classInfo = classInfo; 1017 } 1018 1019 /** 1020 * Gets the AST node representing this exception. 1021 * 1022 * @return the AST node representing this exception 1023 */ 1024 private DetailAST getAst() { 1025 return ast; 1026 } 1027 1028 /** Mark that the exception has associated throws tag. */ 1029 private void setFound() { 1030 found = true; 1031 } 1032 1033 /** 1034 * Checks that the exception has throws tag associated with it. 1035 * 1036 * @return whether the exception has throws tag associated with 1037 */ 1038 private boolean isFound() { 1039 return found; 1040 } 1041 1042 /** 1043 * Gets exception name. 1044 * 1045 * @return exception's name 1046 */ 1047 private Token getName() { 1048 return classInfo.name(); 1049 } 1050 1051 } 1052 1053}