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