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