001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.Set; 029import java.util.regex.MatchResult; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import com.puppycrawl.tools.checkstyle.StatelessCheck; 034import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 035import com.puppycrawl.tools.checkstyle.api.DetailAST; 036import com.puppycrawl.tools.checkstyle.api.FileContents; 037import com.puppycrawl.tools.checkstyle.api.FullIdent; 038import com.puppycrawl.tools.checkstyle.api.TextBlock; 039import com.puppycrawl.tools.checkstyle.api.TokenTypes; 040import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 041import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 042import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 043import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 044import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil; 045 046/** 047 * <div> 048 * Checks the Javadoc of a method or constructor. 049 * </div> 050 * 051 * <p> 052 * Violates parameters and type parameters for which no param tags are present can 053 * be suppressed by defining property {@code allowMissingParamTags}. 054 * </p> 055 * 056 * <p> 057 * Violates methods which return non-void but for which no return tag is present can 058 * be suppressed by defining property {@code allowMissingReturnTag}. 059 * </p> 060 * 061 * <p> 062 * Violates exceptions which are declared to be thrown (by {@code throws} in the method 063 * signature or by {@code throw new} in the method body), but for which no throws tag is 064 * present by activation of property {@code validateThrows}. 065 * Note that {@code throw new} is not checked in the following places: 066 * </p> 067 * <ul> 068 * <li> 069 * Inside a try block (with catch). It is not possible to determine if the thrown 070 * exception can be caught by the catch block as there is no knowledge of the 071 * inheritance hierarchy, so the try block is ignored entirely. However, catch 072 * and finally blocks, as well as try blocks without catch, are still checked. 073 * </li> 074 * <li> 075 * Local classes, anonymous classes and lambda expressions. It is not known when the 076 * throw statements inside such classes are going to be evaluated, so they are ignored. 077 * </li> 078 * </ul> 079 * 080 * <p> 081 * ATTENTION: Checkstyle does not have information about hierarchy of exception types 082 * so usage of base class is considered as separate exception type. 083 * As workaround, you need to specify both types in javadoc (parent and exact type). 084 * </p> 085 * 086 * <p> 087 * Javadoc is not required on a method that is tagged with the {@code @Override} 088 * annotation. However, under Java 5 it is not possible to mark a method required 089 * for an interface (this was <i>corrected</i> under Java 6). Hence, Checkstyle 090 * supports using the convention of using a single {@code {@inheritDoc}} tag 091 * instead of all the other tags. 092 * </p> 093 * 094 * <p> 095 * Note that only inheritable items will allow the {@code {@inheritDoc}} 096 * tag to be used in place of comments. Static methods at all visibilities, 097 * private non-static methods and constructors are not inheritable. 098 * </p> 099 * 100 * <p> 101 * For example, if the following method is implementing a method required by 102 * an interface, then the Javadoc could be done as: 103 * </p> 104 * <pre> 105 * /** {@inheritDoc} */ 106 * public int checkReturnTag(final int aTagIndex, 107 * JavadocTag[] aTags, 108 * int aLineNo) 109 * </pre> 110 * <ul> 111 * <li> 112 * Property {@code accessModifiers} - Specify the access modifiers where Javadoc comments are 113 * checked. 114 * Type is {@code com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption[]}. 115 * Default value is {@code public, protected, package, private}. 116 * </li> 117 * <li> 118 * Property {@code allowMissingParamTags} - Control whether to ignore violations 119 * when a method has parameters but does not have matching {@code param} tags in the javadoc. 120 * Type is {@code boolean}. 121 * Default value is {@code false}. 122 * </li> 123 * <li> 124 * Property {@code allowMissingReturnTag} - Control whether to ignore violations 125 * when a method returns non-void type and does not have a {@code return} tag in the javadoc. 126 * Type is {@code boolean}. 127 * Default value is {@code false}. 128 * </li> 129 * <li> 130 * Property {@code allowedAnnotations} - Specify annotations that allow missed documentation. 131 * Type is {@code java.lang.String[]}. 132 * Default value is {@code Override}. 133 * </li> 134 * <li> 135 * Property {@code validateThrows} - Control whether to validate {@code throws} tags. 136 * Type is {@code boolean}. 137 * Default value is {@code false}. 138 * </li> 139 * <li> 140 * Property {@code tokens} - tokens to check 141 * Type is {@code java.lang.String[]}. 142 * Validation type is {@code tokenSet}. 143 * Default value is: 144 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF"> 145 * METHOD_DEF</a>, 146 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF"> 147 * CTOR_DEF</a>, 148 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATION_FIELD_DEF"> 149 * ANNOTATION_FIELD_DEF</a>, 150 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF"> 151 * COMPACT_CTOR_DEF</a>. 152 * </li> 153 * </ul> 154 * 155 * <p> 156 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 157 * </p> 158 * 159 * <p> 160 * Violation Message Keys: 161 * </p> 162 * <ul> 163 * <li> 164 * {@code javadoc.classInfo} 165 * </li> 166 * <li> 167 * {@code javadoc.duplicateTag} 168 * </li> 169 * <li> 170 * {@code javadoc.expectedTag} 171 * </li> 172 * <li> 173 * {@code javadoc.invalidInheritDoc} 174 * </li> 175 * <li> 176 * {@code javadoc.return.expected} 177 * </li> 178 * <li> 179 * {@code javadoc.unusedTag} 180 * </li> 181 * <li> 182 * {@code javadoc.unusedTagGeneral} 183 * </li> 184 * </ul> 185 * 186 * @since 3.0 187 */ 188@StatelessCheck 189public class JavadocMethodCheck extends AbstractCheck { 190 191 /** 192 * A key is pointing to the warning message text in "messages.properties" 193 * file. 194 */ 195 public static final String MSG_CLASS_INFO = "javadoc.classInfo"; 196 197 /** 198 * A key is pointing to the warning message text in "messages.properties" 199 * file. 200 */ 201 public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral"; 202 203 /** 204 * A key is pointing to the warning message text in "messages.properties" 205 * file. 206 */ 207 public static final String MSG_INVALID_INHERIT_DOC = "javadoc.invalidInheritDoc"; 208 209 /** 210 * A key is pointing to the warning message text in "messages.properties" 211 * file. 212 */ 213 public static final String MSG_UNUSED_TAG = "javadoc.unusedTag"; 214 215 /** 216 * A key is pointing to the warning message text in "messages.properties" 217 * file. 218 */ 219 public static final String MSG_EXPECTED_TAG = "javadoc.expectedTag"; 220 221 /** 222 * A key is pointing to the warning message text in "messages.properties" 223 * file. 224 */ 225 public static final String MSG_RETURN_EXPECTED = "javadoc.return.expected"; 226 227 /** 228 * A key is pointing to the warning message text in "messages.properties" 229 * file. 230 */ 231 public static final String MSG_DUPLICATE_TAG = "javadoc.duplicateTag"; 232 233 /** Html element start symbol. */ 234 private static final String ELEMENT_START = "<"; 235 236 /** Html element end symbol. */ 237 private static final String ELEMENT_END = ">"; 238 239 /** Compiled regexp to match Javadoc tags that take an argument. */ 240 private static final Pattern MATCH_JAVADOC_ARG = CommonUtil.createPattern( 241 "^\\s*(?>\\*|\\/\\*\\*)?\\s*@(throws|exception|param)\\s+(\\S+)\\s+\\S*"); 242 /** Compiled regexp to match Javadoc tags with argument but with missing description. */ 243 private static final Pattern MATCH_JAVADOC_ARG_MISSING_DESCRIPTION = 244 CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(throws|exception|param)\\s+" 245 + "(\\S[^*]*)(?:(\\s+|\\*\\/))?"); 246 247 /** Compiled regexp to look for a continuation of the comment. */ 248 private static final Pattern MATCH_JAVADOC_MULTILINE_CONT = 249 CommonUtil.createPattern("(\\*\\/|@|[^\\s\\*])"); 250 251 /** Multiline finished at end of comment. */ 252 private static final String END_JAVADOC = "*/"; 253 /** Multiline finished at next Javadoc. */ 254 private static final String NEXT_TAG = "@"; 255 256 /** Compiled regexp to match Javadoc tags with no argument. */ 257 private static final Pattern MATCH_JAVADOC_NOARG = 258 CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(return|see)\\s+\\S"); 259 /** Compiled regexp to match first part of multilineJavadoc tags. */ 260 private static final Pattern MATCH_JAVADOC_NOARG_MULTILINE_START = 261 CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(return|see)\\s*$"); 262 /** Compiled regexp to match Javadoc tags with no argument and {}. */ 263 private static final Pattern MATCH_JAVADOC_NOARG_CURLY = 264 CommonUtil.createPattern("\\{\\s*@(inheritDoc)\\s*\\}"); 265 266 /** Specify the access modifiers where Javadoc comments are checked. */ 267 private AccessModifierOption[] accessModifiers = { 268 AccessModifierOption.PUBLIC, 269 AccessModifierOption.PROTECTED, 270 AccessModifierOption.PACKAGE, 271 AccessModifierOption.PRIVATE, 272 }; 273 274 /** 275 * Control whether to validate {@code throws} tags. 276 */ 277 private boolean validateThrows; 278 279 /** 280 * Control whether to ignore violations when a method has parameters but does 281 * not have matching {@code param} tags in the javadoc. 282 */ 283 private boolean allowMissingParamTags; 284 285 /** 286 * Control whether to ignore violations when a method returns non-void type 287 * and does not have a {@code return} tag in the javadoc. 288 */ 289 private boolean allowMissingReturnTag; 290 291 /** Specify annotations that allow missed documentation. */ 292 private Set<String> allowedAnnotations = Set.of("Override"); 293 294 /** 295 * Setter to control whether to validate {@code throws} tags. 296 * 297 * @param value user's value. 298 * @since 6.0 299 */ 300 public void setValidateThrows(boolean value) { 301 validateThrows = value; 302 } 303 304 /** 305 * Setter to specify annotations that allow missed documentation. 306 * 307 * @param userAnnotations user's value. 308 * @since 6.0 309 */ 310 public void setAllowedAnnotations(String... userAnnotations) { 311 allowedAnnotations = Set.of(userAnnotations); 312 } 313 314 /** 315 * Setter to specify the access modifiers where Javadoc comments are checked. 316 * 317 * @param accessModifiers access modifiers. 318 * @since 8.42 319 */ 320 public void setAccessModifiers(AccessModifierOption... accessModifiers) { 321 this.accessModifiers = 322 UnmodifiableCollectionUtil.copyOfArray(accessModifiers, accessModifiers.length); 323 } 324 325 /** 326 * Setter to control whether to ignore violations when a method has parameters 327 * but does not have matching {@code param} tags in the javadoc. 328 * 329 * @param flag a {@code Boolean} value 330 * @since 3.1 331 */ 332 public void setAllowMissingParamTags(boolean flag) { 333 allowMissingParamTags = flag; 334 } 335 336 /** 337 * Setter to control whether to ignore violations when a method returns non-void type 338 * and does not have a {@code return} tag in the javadoc. 339 * 340 * @param flag a {@code Boolean} value 341 * @since 3.1 342 */ 343 public void setAllowMissingReturnTag(boolean flag) { 344 allowMissingReturnTag = flag; 345 } 346 347 @Override 348 public final int[] getRequiredTokens() { 349 return CommonUtil.EMPTY_INT_ARRAY; 350 } 351 352 @Override 353 public int[] getDefaultTokens() { 354 return getAcceptableTokens(); 355 } 356 357 @Override 358 public int[] getAcceptableTokens() { 359 return new int[] { 360 TokenTypes.METHOD_DEF, 361 TokenTypes.CTOR_DEF, 362 TokenTypes.ANNOTATION_FIELD_DEF, 363 TokenTypes.COMPACT_CTOR_DEF, 364 }; 365 } 366 367 @Override 368 public final void visitToken(DetailAST ast) { 369 processAST(ast); 370 } 371 372 /** 373 * Called to process an AST when visiting it. 374 * 375 * @param ast the AST to process. Guaranteed to not be PACKAGE_DEF or 376 * IMPORT tokens. 377 */ 378 // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166 379 @SuppressWarnings("deprecation") 380 private void processAST(DetailAST ast) { 381 if (shouldCheck(ast)) { 382 final FileContents contents = getFileContents(); 383 final TextBlock textBlock = contents.getJavadocBefore(ast.getLineNo()); 384 385 if (textBlock != null) { 386 checkComment(ast, textBlock); 387 } 388 } 389 } 390 391 /** 392 * Whether we should check this node. 393 * 394 * @param ast a given node. 395 * @return whether we should check a given node. 396 */ 397 private boolean shouldCheck(final DetailAST ast) { 398 final AccessModifierOption surroundingAccessModifier = CheckUtil 399 .getSurroundingAccessModifier(ast); 400 final AccessModifierOption accessModifier = CheckUtil 401 .getAccessModifierFromModifiersToken(ast); 402 return Arrays.stream(accessModifiers) 403 .anyMatch(modifier -> modifier == surroundingAccessModifier) 404 && Arrays.stream(accessModifiers).anyMatch(modifier -> modifier == accessModifier); 405 } 406 407 /** 408 * Checks the Javadoc for a method. 409 * 410 * @param ast the token for the method 411 * @param comment the Javadoc comment 412 */ 413 private void checkComment(DetailAST ast, TextBlock comment) { 414 final List<JavadocTag> tags = getMethodTags(comment); 415 416 if (!hasShortCircuitTag(ast, tags)) { 417 if (ast.getType() == TokenTypes.ANNOTATION_FIELD_DEF) { 418 checkReturnTag(tags, ast.getLineNo(), true); 419 } 420 else { 421 final Iterator<JavadocTag> it = tags.iterator(); 422 // Check for inheritDoc 423 boolean hasInheritDocTag = false; 424 while (!hasInheritDocTag && it.hasNext()) { 425 hasInheritDocTag = it.next().isInheritDocTag(); 426 } 427 final boolean reportExpectedTags = !hasInheritDocTag 428 && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations); 429 430 // COMPACT_CTOR_DEF has no parameters 431 if (ast.getType() == TokenTypes.COMPACT_CTOR_DEF) { 432 checkRecordParamTags(tags, ast, reportExpectedTags); 433 } 434 else { 435 checkParamTags(tags, ast, reportExpectedTags); 436 } 437 final List<ExceptionInfo> throwed = 438 combineExceptionInfo(getThrows(ast), getThrowed(ast)); 439 checkThrowsTags(tags, throwed, reportExpectedTags); 440 if (CheckUtil.isNonVoidMethod(ast)) { 441 checkReturnTag(tags, ast.getLineNo(), reportExpectedTags); 442 } 443 } 444 } 445 tags.stream().filter(javadocTag -> !javadocTag.isSeeOrInheritDocTag()) 446 .forEach(javadocTag -> log(javadocTag.getLineNo(), MSG_UNUSED_TAG_GENERAL)); 447 } 448 449 /** 450 * Retrieves the list of record components from a given record definition. 451 * 452 * @param recordDef the AST node representing the record definition 453 * @return a list of AST nodes representing the record components 454 */ 455 private static List<DetailAST> getRecordComponents(final DetailAST recordDef) { 456 final List<DetailAST> components = new ArrayList<>(); 457 final DetailAST recordDecl = recordDef.findFirstToken(TokenTypes.RECORD_COMPONENTS); 458 459 DetailAST child = recordDecl.getFirstChild(); 460 while (child != null) { 461 if (child.getType() == TokenTypes.RECORD_COMPONENT_DEF) { 462 components.add(child.findFirstToken(TokenTypes.IDENT)); 463 } 464 child = child.getNextSibling(); 465 } 466 return components; 467 } 468 469 /** 470 * Finds the nearest ancestor record definition node for the given AST node. 471 * 472 * @param ast the AST node to start searching from 473 * @return the nearest {@code RECORD_DEF} AST node, or {@code null} if not found 474 */ 475 private static DetailAST getRecordDef(DetailAST ast) { 476 DetailAST current = ast; 477 while (current.getType() != TokenTypes.RECORD_DEF) { 478 current = current.getParent(); 479 } 480 return current; 481 } 482 483 /** 484 * Validates whether the Javadoc has a short circuit tag. Currently, this is 485 * the inheritTag. Any violations are logged. 486 * 487 * @param ast the construct being checked 488 * @param tags the list of Javadoc tags associated with the construct 489 * @return true if the construct has a short circuit tag. 490 */ 491 private boolean hasShortCircuitTag(final DetailAST ast, final List<JavadocTag> tags) { 492 boolean result = true; 493 // Check if it contains {@inheritDoc} tag 494 if (tags.size() == 1 495 && tags.get(0).isInheritDocTag()) { 496 // Invalid if private, a constructor, or a static method 497 if (!JavadocTagInfo.INHERIT_DOC.isValidOn(ast)) { 498 log(ast, MSG_INVALID_INHERIT_DOC); 499 } 500 } 501 else { 502 result = false; 503 } 504 return result; 505 } 506 507 /** 508 * Returns the tags in a javadoc comment. Only finds throws, exception, 509 * param, return and see tags. 510 * 511 * @param comment the Javadoc comment 512 * @return the tags found 513 */ 514 private static List<JavadocTag> getMethodTags(TextBlock comment) { 515 final String[] lines = comment.getText(); 516 final List<JavadocTag> tags = new ArrayList<>(); 517 int currentLine = comment.getStartLineNo() - 1; 518 final int startColumnNumber = comment.getStartColNo(); 519 520 for (int i = 0; i < lines.length; i++) { 521 currentLine++; 522 final Matcher javadocArgMatcher = 523 MATCH_JAVADOC_ARG.matcher(lines[i]); 524 final Matcher javadocArgMissingDescriptionMatcher = 525 MATCH_JAVADOC_ARG_MISSING_DESCRIPTION.matcher(lines[i]); 526 final Matcher javadocNoargMatcher = 527 MATCH_JAVADOC_NOARG.matcher(lines[i]); 528 final Matcher noargCurlyMatcher = 529 MATCH_JAVADOC_NOARG_CURLY.matcher(lines[i]); 530 final Matcher noargMultilineStart = 531 MATCH_JAVADOC_NOARG_MULTILINE_START.matcher(lines[i]); 532 533 if (javadocArgMatcher.find()) { 534 final int col = calculateTagColumn(javadocArgMatcher, i, startColumnNumber); 535 tags.add(new JavadocTag(currentLine, col, javadocArgMatcher.group(1), 536 javadocArgMatcher.group(2))); 537 } 538 else if (javadocArgMissingDescriptionMatcher.find()) { 539 final int col = calculateTagColumn(javadocArgMissingDescriptionMatcher, i, 540 startColumnNumber); 541 tags.add(new JavadocTag(currentLine, col, 542 javadocArgMissingDescriptionMatcher.group(1), 543 javadocArgMissingDescriptionMatcher.group(2))); 544 } 545 else if (javadocNoargMatcher.find()) { 546 final int col = calculateTagColumn(javadocNoargMatcher, i, startColumnNumber); 547 tags.add(new JavadocTag(currentLine, col, javadocNoargMatcher.group(1))); 548 } 549 else if (noargCurlyMatcher.find()) { 550 tags.add(new JavadocTag(currentLine, 0, noargCurlyMatcher.group(1))); 551 } 552 else if (noargMultilineStart.find()) { 553 tags.addAll(getMultilineNoArgTags(noargMultilineStart, lines, i, currentLine)); 554 } 555 } 556 return tags; 557 } 558 559 /** 560 * Calculates column number using Javadoc tag matcher. 561 * 562 * @param javadocTagMatchResult found javadoc tag match result 563 * @param lineNumber line number of Javadoc tag in comment 564 * @param startColumnNumber column number of Javadoc comment beginning 565 * @return column number 566 */ 567 private static int calculateTagColumn(MatchResult javadocTagMatchResult, 568 int lineNumber, int startColumnNumber) { 569 int col = javadocTagMatchResult.start(1) - 1; 570 if (lineNumber == 0) { 571 col += startColumnNumber; 572 } 573 return col; 574 } 575 576 /** 577 * Gets multiline Javadoc tags with no arguments. 578 * 579 * @param noargMultilineStart javadoc tag Matcher 580 * @param lines comment text lines 581 * @param lineIndex line number that contains the javadoc tag 582 * @param tagLine javadoc tag line number in file 583 * @return javadoc tags with no arguments 584 */ 585 private static List<JavadocTag> getMultilineNoArgTags(final Matcher noargMultilineStart, 586 final String[] lines, final int lineIndex, final int tagLine) { 587 int remIndex = lineIndex; 588 Matcher multilineCont; 589 590 do { 591 remIndex++; 592 multilineCont = MATCH_JAVADOC_MULTILINE_CONT.matcher(lines[remIndex]); 593 } while (!multilineCont.find()); 594 595 final List<JavadocTag> tags = new ArrayList<>(); 596 final String lFin = multilineCont.group(1); 597 if (!NEXT_TAG.equals(lFin) 598 && !END_JAVADOC.equals(lFin)) { 599 final String param1 = noargMultilineStart.group(1); 600 final int col = noargMultilineStart.start(1) - 1; 601 602 tags.add(new JavadocTag(tagLine, col, param1)); 603 } 604 605 return tags; 606 } 607 608 /** 609 * Computes the parameter nodes for a method. 610 * 611 * @param ast the method node. 612 * @return the list of parameter nodes for ast. 613 */ 614 private static List<DetailAST> getParameters(DetailAST ast) { 615 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS); 616 final List<DetailAST> returnValue = new ArrayList<>(); 617 618 DetailAST child = params.getFirstChild(); 619 while (child != null) { 620 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT); 621 if (ident != null) { 622 returnValue.add(ident); 623 } 624 child = child.getNextSibling(); 625 } 626 return returnValue; 627 } 628 629 /** 630 * Computes the exception nodes for a method. 631 * 632 * @param ast the method node. 633 * @return the list of exception nodes for ast. 634 */ 635 private static List<ExceptionInfo> getThrows(DetailAST ast) { 636 final List<ExceptionInfo> returnValue = new ArrayList<>(); 637 final DetailAST throwsAST = ast 638 .findFirstToken(TokenTypes.LITERAL_THROWS); 639 if (throwsAST != null) { 640 DetailAST child = throwsAST.getFirstChild(); 641 while (child != null) { 642 if (child.getType() == TokenTypes.IDENT 643 || child.getType() == TokenTypes.DOT) { 644 returnValue.add(getExceptionInfo(child)); 645 } 646 child = child.getNextSibling(); 647 } 648 } 649 return returnValue; 650 } 651 652 /** 653 * Get ExceptionInfo for all exceptions that throws in method code by 'throw new'. 654 * 655 * @param methodAst method DetailAST object where to find exceptions 656 * @return list of ExceptionInfo 657 */ 658 private static List<ExceptionInfo> getThrowed(DetailAST methodAst) { 659 final List<ExceptionInfo> returnValue = new ArrayList<>(); 660 final List<DetailAST> throwLiterals = findTokensInAstByType(methodAst, 661 TokenTypes.LITERAL_THROW); 662 for (DetailAST throwAst : throwLiterals) { 663 if (!isInIgnoreBlock(methodAst, throwAst)) { 664 final DetailAST newAst = throwAst.getFirstChild().getFirstChild(); 665 if (newAst.getType() == TokenTypes.LITERAL_NEW) { 666 final DetailAST child = newAst.getFirstChild(); 667 returnValue.add(getExceptionInfo(child)); 668 } 669 } 670 } 671 return returnValue; 672 } 673 674 /** 675 * Get ExceptionInfo instance. 676 * 677 * @param ast DetailAST object where to find exceptions node; 678 * @return ExceptionInfo 679 */ 680 private static ExceptionInfo getExceptionInfo(DetailAST ast) { 681 final FullIdent ident = FullIdent.createFullIdent(ast); 682 final DetailAST firstClassNameNode = getFirstClassNameNode(ast); 683 return new ExceptionInfo(firstClassNameNode, 684 new ClassInfo(new Token(ident))); 685 } 686 687 /** 688 * Get node where class name of exception starts. 689 * 690 * @param ast DetailAST object where to find exceptions node; 691 * @return exception node where class name starts 692 */ 693 private static DetailAST getFirstClassNameNode(DetailAST ast) { 694 DetailAST startNode = ast; 695 while (startNode.getType() == TokenTypes.DOT) { 696 startNode = startNode.getFirstChild(); 697 } 698 return startNode; 699 } 700 701 /** 702 * Checks if a 'throw' usage is contained within a block that should be ignored. 703 * Such blocks consist of try (with catch) blocks, local classes, anonymous classes, 704 * and lambda expressions. Note that a try block without catch is not considered. 705 * 706 * @param methodBodyAst DetailAST node representing the method body 707 * @param throwAst DetailAST node representing the 'throw' literal 708 * @return true if throwAst is inside a block that should be ignored 709 */ 710 private static boolean isInIgnoreBlock(DetailAST methodBodyAst, DetailAST throwAst) { 711 DetailAST ancestor = throwAst; 712 while (ancestor != methodBodyAst) { 713 if (ancestor.getType() == TokenTypes.LAMBDA 714 || ancestor.getType() == TokenTypes.OBJBLOCK 715 || ancestor.findFirstToken(TokenTypes.LITERAL_CATCH) != null) { 716 // throw is inside a lambda expression/anonymous class/local class, 717 // or throw is inside a try block, and there is a catch block 718 break; 719 } 720 if (ancestor.getType() == TokenTypes.LITERAL_CATCH 721 || ancestor.getType() == TokenTypes.LITERAL_FINALLY) { 722 // if the throw is inside a catch or finally block, 723 // skip the immediate ancestor (try token) 724 ancestor = ancestor.getParent(); 725 } 726 ancestor = ancestor.getParent(); 727 } 728 return ancestor != methodBodyAst; 729 } 730 731 /** 732 * Combine ExceptionInfo collections together by matching names. 733 * 734 * @param first the first collection of ExceptionInfo 735 * @param second the second collection of ExceptionInfo 736 * @return combined list of ExceptionInfo 737 */ 738 private static List<ExceptionInfo> combineExceptionInfo(Collection<ExceptionInfo> first, 739 Iterable<ExceptionInfo> second) { 740 final List<ExceptionInfo> result = new ArrayList<>(first); 741 for (ExceptionInfo exceptionInfo : second) { 742 if (result.stream().noneMatch(item -> isExceptionInfoSame(item, exceptionInfo))) { 743 result.add(exceptionInfo); 744 } 745 } 746 return result; 747 } 748 749 /** 750 * Finds node of specified type among root children, siblings, siblings children 751 * on any deep level. 752 * 753 * @param root DetailAST 754 * @param astType value of TokenType 755 * @return {@link List} of {@link DetailAST} nodes which matches the predicate. 756 */ 757 public static List<DetailAST> findTokensInAstByType(DetailAST root, int astType) { 758 final List<DetailAST> result = new ArrayList<>(); 759 // iterative preorder depth-first search 760 DetailAST curNode = root; 761 do { 762 // process curNode 763 if (curNode.getType() == astType) { 764 result.add(curNode); 765 } 766 // process children (if any) 767 if (curNode.hasChildren()) { 768 curNode = curNode.getFirstChild(); 769 continue; 770 } 771 // backtrack to parent if last child, stopping at root 772 while (curNode.getNextSibling() == null) { 773 curNode = curNode.getParent(); 774 } 775 // explore siblings if not root 776 if (curNode != root) { 777 curNode = curNode.getNextSibling(); 778 } 779 } while (curNode != root); 780 return result; 781 } 782 783 /** 784 * Checks if all record components in a compact constructor have 785 * corresponding {@code @param} tags. 786 * Reports missing or extra {@code @param} tags in the Javadoc. 787 * 788 * @param tags the list of Javadoc tags 789 * @param compactDef the compact constructor AST node 790 * @param reportExpectedTags whether to report missing {@code @param} tags 791 */ 792 private void checkRecordParamTags(final List<JavadocTag> tags, 793 final DetailAST compactDef, boolean reportExpectedTags) { 794 795 final DetailAST parent = getRecordDef(compactDef); 796 final List<DetailAST> params = getRecordComponents(parent); 797 798 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 799 while (tagIt.hasNext()) { 800 final JavadocTag tag = tagIt.next(); 801 802 if (!tag.isParamTag()) { 803 continue; 804 } 805 806 tagIt.remove(); 807 808 final String arg1 = tag.getFirstArg(); 809 final boolean found = removeMatchingParam(params, arg1); 810 811 if (!found) { 812 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, 813 JavadocTagInfo.PARAM.getText(), arg1); 814 } 815 } 816 817 if (!allowMissingParamTags && reportExpectedTags) { 818 for (DetailAST param : params) { 819 log(compactDef, MSG_EXPECTED_TAG, 820 JavadocTagInfo.PARAM.getText(), param.getText()); 821 } 822 } 823 } 824 825 /** 826 * Checks a set of tags for matching parameters. 827 * 828 * @param tags the tags to check 829 * @param parent the node which takes the parameters 830 * @param reportExpectedTags whether we should report if do not find 831 * expected tag 832 */ 833 private void checkParamTags(final List<JavadocTag> tags, 834 final DetailAST parent, boolean reportExpectedTags) { 835 final List<DetailAST> params = getParameters(parent); 836 final List<DetailAST> typeParams = CheckUtil 837 .getTypeParameters(parent); 838 839 // Loop over the tags, checking to see they exist in the params. 840 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 841 while (tagIt.hasNext()) { 842 final JavadocTag tag = tagIt.next(); 843 844 if (!tag.isParamTag()) { 845 continue; 846 } 847 848 tagIt.remove(); 849 850 final String arg1 = tag.getFirstArg(); 851 boolean found = removeMatchingParam(params, arg1); 852 853 if (arg1.endsWith(ELEMENT_END)) { 854 found = searchMatchingTypeParameter(typeParams, 855 arg1.substring(1, arg1.length() - 1)); 856 } 857 858 // Handle extra JavadocTag 859 if (!found) { 860 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, 861 JavadocTagInfo.PARAM.getText(), arg1); 862 } 863 } 864 865 // Now dump out all type parameters/parameters without tags :- unless 866 // the user has chosen to suppress these problems 867 if (!allowMissingParamTags && reportExpectedTags) { 868 for (DetailAST param : params) { 869 log(param, MSG_EXPECTED_TAG, 870 JavadocTagInfo.PARAM.getText(), param.getText()); 871 } 872 873 for (DetailAST typeParam : typeParams) { 874 log(typeParam, MSG_EXPECTED_TAG, 875 JavadocTagInfo.PARAM.getText(), 876 ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText() 877 + ELEMENT_END); 878 } 879 } 880 } 881 882 /** 883 * Returns true if required type found in type parameters. 884 * 885 * @param typeParams 886 * collection of type parameters 887 * @param requiredTypeName 888 * name of required type 889 * @return true if required type found in type parameters. 890 */ 891 private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams, 892 String requiredTypeName) { 893 // Loop looking for matching type param 894 final Iterator<DetailAST> typeParamsIt = typeParams.iterator(); 895 boolean found = false; 896 while (typeParamsIt.hasNext()) { 897 final DetailAST typeParam = typeParamsIt.next(); 898 if (typeParam.findFirstToken(TokenTypes.IDENT).getText() 899 .equals(requiredTypeName)) { 900 found = true; 901 typeParamsIt.remove(); 902 break; 903 } 904 } 905 return found; 906 } 907 908 /** 909 * Remove parameter from params collection by name. 910 * 911 * @param params collection of DetailAST parameters 912 * @param paramName name of parameter 913 * @return true if parameter found and removed 914 */ 915 private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) { 916 boolean found = false; 917 final Iterator<DetailAST> paramIt = params.iterator(); 918 while (paramIt.hasNext()) { 919 final DetailAST param = paramIt.next(); 920 if (param.getText().equals(paramName)) { 921 found = true; 922 paramIt.remove(); 923 break; 924 } 925 } 926 return found; 927 } 928 929 /** 930 * Checks for only one return tag. All return tags will be removed from the 931 * supplied list. 932 * 933 * @param tags the tags to check 934 * @param lineNo the line number of the expected tag 935 * @param reportExpectedTags whether we should report if do not find 936 * expected tag 937 */ 938 private void checkReturnTag(List<JavadocTag> tags, int lineNo, 939 boolean reportExpectedTags) { 940 // Loop over tags finding return tags. After the first one, report a 941 // violation. 942 boolean found = false; 943 final ListIterator<JavadocTag> it = tags.listIterator(); 944 while (it.hasNext()) { 945 final JavadocTag javadocTag = it.next(); 946 if (javadocTag.isReturnTag()) { 947 if (found) { 948 log(javadocTag.getLineNo(), javadocTag.getColumnNo(), 949 MSG_DUPLICATE_TAG, 950 JavadocTagInfo.RETURN.getText()); 951 } 952 found = true; 953 it.remove(); 954 } 955 } 956 957 // Handle there being no @return tags :- unless 958 // the user has chosen to suppress these problems 959 if (!found && !allowMissingReturnTag && reportExpectedTags) { 960 log(lineNo, MSG_RETURN_EXPECTED); 961 } 962 } 963 964 /** 965 * Checks a set of tags for matching throws. 966 * 967 * @param tags the tags to check 968 * @param throwsList the throws to check 969 * @param reportExpectedTags whether we should report if do not find 970 * expected tag 971 */ 972 private void checkThrowsTags(List<JavadocTag> tags, 973 List<ExceptionInfo> throwsList, boolean reportExpectedTags) { 974 // Loop over the tags, checking to see they exist in the throws. 975 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 976 while (tagIt.hasNext()) { 977 final JavadocTag tag = tagIt.next(); 978 979 if (!tag.isThrowsTag()) { 980 continue; 981 } 982 tagIt.remove(); 983 984 // Loop looking for matching throw 985 processThrows(throwsList, tag.getFirstArg()); 986 } 987 // Now dump out all throws without tags :- unless 988 // the user has chosen to suppress these problems 989 if (validateThrows && reportExpectedTags) { 990 throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound()) 991 .forEach(exceptionInfo -> { 992 final Token token = exceptionInfo.getName(); 993 log(exceptionInfo.getAst(), 994 MSG_EXPECTED_TAG, 995 JavadocTagInfo.THROWS.getText(), token.getText()); 996 }); 997 } 998 } 999 1000 /** 1001 * Verifies that documented exception is in throws. 1002 * 1003 * @param throwsIterable collection of throws 1004 * @param documentedClassName documented exception class name 1005 */ 1006 private static void processThrows(Iterable<ExceptionInfo> throwsIterable, 1007 String documentedClassName) { 1008 for (ExceptionInfo exceptionInfo : throwsIterable) { 1009 if (isClassNamesSame(exceptionInfo.getName().getText(), 1010 documentedClassName)) { 1011 exceptionInfo.setFound(); 1012 break; 1013 } 1014 } 1015 } 1016 1017 /** 1018 * Check that ExceptionInfo objects are same by name. 1019 * 1020 * @param info1 ExceptionInfo object 1021 * @param info2 ExceptionInfo object 1022 * @return true is ExceptionInfo object have the same name 1023 */ 1024 private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) { 1025 return isClassNamesSame(info1.getName().getText(), 1026 info2.getName().getText()); 1027 } 1028 1029 /** 1030 * Check that class names are same by short name of class. If some class name is fully 1031 * qualified it is cut to short name. 1032 * 1033 * @param class1 class name 1034 * @param class2 class name 1035 * @return true is ExceptionInfo object have the same name 1036 */ 1037 private static boolean isClassNamesSame(String class1, String class2) { 1038 final String class1ShortName = class1 1039 .substring(class1.lastIndexOf('.') + 1); 1040 final String class2ShortName = class2 1041 .substring(class2.lastIndexOf('.') + 1); 1042 return class1ShortName.equals(class2ShortName); 1043 } 1044 1045 /** 1046 * Contains class's {@code Token}. 1047 */ 1048 private static class ClassInfo { 1049 1050 /** {@code FullIdent} associated with this class. */ 1051 private final Token name; 1052 1053 /** 1054 * Creates new instance of class information object. 1055 * 1056 * @param className token which represents class name. 1057 * @throws IllegalArgumentException when className is nulls 1058 */ 1059 protected ClassInfo(final Token className) { 1060 name = className; 1061 } 1062 1063 /** 1064 * Gets class name. 1065 * 1066 * @return class name 1067 */ 1068 public final Token getName() { 1069 return name; 1070 } 1071 1072 } 1073 1074 /** 1075 * Represents text element with location in the text. 1076 */ 1077 private static final class Token { 1078 1079 /** Token's column number. */ 1080 private final int columnNo; 1081 /** Token's line number. */ 1082 private final int lineNo; 1083 /** Token's text. */ 1084 private final String text; 1085 1086 /** 1087 * Creates token. 1088 * 1089 * @param text token's text 1090 * @param lineNo token's line number 1091 * @param columnNo token's column number 1092 */ 1093 private Token(String text, int lineNo, int columnNo) { 1094 this.text = text; 1095 this.lineNo = lineNo; 1096 this.columnNo = columnNo; 1097 } 1098 1099 /** 1100 * Converts FullIdent to Token. 1101 * 1102 * @param fullIdent full ident to convert. 1103 */ 1104 private Token(FullIdent fullIdent) { 1105 text = fullIdent.getText(); 1106 lineNo = fullIdent.getLineNo(); 1107 columnNo = fullIdent.getColumnNo(); 1108 } 1109 1110 /** 1111 * Gets text of the token. 1112 * 1113 * @return text of the token 1114 */ 1115 public String getText() { 1116 return text; 1117 } 1118 1119 @Override 1120 public String toString() { 1121 return "Token[" + text + "(" + lineNo 1122 + "x" + columnNo + ")]"; 1123 } 1124 1125 } 1126 1127 /** Stores useful information about declared exception. */ 1128 private static final class ExceptionInfo { 1129 1130 /** AST node representing this exception. */ 1131 private final DetailAST ast; 1132 1133 /** Class information associated with this exception. */ 1134 private final ClassInfo classInfo; 1135 /** Does the exception have throws tag associated with. */ 1136 private boolean found; 1137 1138 /** 1139 * Creates new instance for {@code FullIdent}. 1140 * 1141 * @param ast AST node representing this exception 1142 * @param classInfo class info 1143 */ 1144 private ExceptionInfo(DetailAST ast, ClassInfo classInfo) { 1145 this.ast = ast; 1146 this.classInfo = classInfo; 1147 } 1148 1149 /** 1150 * Gets the AST node representing this exception. 1151 * 1152 * @return the AST node representing this exception 1153 */ 1154 private DetailAST getAst() { 1155 return ast; 1156 } 1157 1158 /** Mark that the exception has associated throws tag. */ 1159 private void setFound() { 1160 found = true; 1161 } 1162 1163 /** 1164 * Checks that the exception has throws tag associated with it. 1165 * 1166 * @return whether the exception has throws tag associated with 1167 */ 1168 private boolean isFound() { 1169 return found; 1170 } 1171 1172 /** 1173 * Gets exception name. 1174 * 1175 * @return exception's name 1176 */ 1177 private Token getName() { 1178 return classInfo.getName(); 1179 } 1180 1181 } 1182 1183}