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