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