001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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 if (child.getType() == TokenTypes.PARAMETER_DEF) { 594 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT); 595 if (ident != null) { 596 returnValue.add(ident); 597 } 598 } 599 child = child.getNextSibling(); 600 } 601 return returnValue; 602 } 603 604 /** 605 * Computes the exception nodes for a method. 606 * 607 * @param ast the method node. 608 * @return the list of exception nodes for ast. 609 */ 610 private static List<ExceptionInfo> getThrows(DetailAST ast) { 611 final List<ExceptionInfo> returnValue = new ArrayList<>(); 612 final DetailAST throwsAST = ast 613 .findFirstToken(TokenTypes.LITERAL_THROWS); 614 if (throwsAST != null) { 615 DetailAST child = throwsAST.getFirstChild(); 616 while (child != null) { 617 if (child.getType() == TokenTypes.IDENT 618 || child.getType() == TokenTypes.DOT) { 619 returnValue.add(getExceptionInfo(child)); 620 } 621 child = child.getNextSibling(); 622 } 623 } 624 return returnValue; 625 } 626 627 /** 628 * Get ExceptionInfo for all exceptions that throws in method code by 'throw new'. 629 * 630 * @param methodAst method DetailAST object where to find exceptions 631 * @return list of ExceptionInfo 632 */ 633 private static List<ExceptionInfo> getThrowed(DetailAST methodAst) { 634 final List<ExceptionInfo> returnValue = new ArrayList<>(); 635 final DetailAST blockAst = methodAst.findFirstToken(TokenTypes.SLIST); 636 if (blockAst != null) { 637 final List<DetailAST> throwLiterals = findTokensInAstByType(blockAst, 638 TokenTypes.LITERAL_THROW); 639 for (DetailAST throwAst : throwLiterals) { 640 if (!isInIgnoreBlock(blockAst, throwAst)) { 641 final DetailAST newAst = throwAst.getFirstChild().getFirstChild(); 642 if (newAst.getType() == TokenTypes.LITERAL_NEW) { 643 final DetailAST child = newAst.getFirstChild(); 644 returnValue.add(getExceptionInfo(child)); 645 } 646 } 647 } 648 } 649 return returnValue; 650 } 651 652 /** 653 * Get ExceptionInfo instance. 654 * 655 * @param ast DetailAST object where to find exceptions node; 656 * @return ExceptionInfo 657 */ 658 private static ExceptionInfo getExceptionInfo(DetailAST ast) { 659 final FullIdent ident = FullIdent.createFullIdent(ast); 660 final DetailAST firstClassNameNode = getFirstClassNameNode(ast); 661 return new ExceptionInfo(firstClassNameNode, 662 new ClassInfo(new Token(ident))); 663 } 664 665 /** 666 * Get node where class name of exception starts. 667 * 668 * @param ast DetailAST object where to find exceptions node; 669 * @return exception node where class name starts 670 */ 671 private static DetailAST getFirstClassNameNode(DetailAST ast) { 672 DetailAST startNode = ast; 673 while (startNode.getType() == TokenTypes.DOT) { 674 startNode = startNode.getFirstChild(); 675 } 676 return startNode; 677 } 678 679 /** 680 * Checks if a 'throw' usage is contained within a block that should be ignored. 681 * Such blocks consist of try (with catch) blocks, local classes, anonymous classes, 682 * and lambda expressions. Note that a try block without catch is not considered. 683 * 684 * @param methodBodyAst DetailAST node representing the method body 685 * @param throwAst DetailAST node representing the 'throw' literal 686 * @return true if throwAst is inside a block that should be ignored 687 */ 688 private static boolean isInIgnoreBlock(DetailAST methodBodyAst, DetailAST throwAst) { 689 DetailAST ancestor = throwAst.getParent(); 690 while (ancestor != methodBodyAst) { 691 if (ancestor.getType() == TokenTypes.LITERAL_TRY 692 && ancestor.findFirstToken(TokenTypes.LITERAL_CATCH) != null 693 || ancestor.getType() == TokenTypes.LAMBDA 694 || ancestor.getType() == TokenTypes.OBJBLOCK) { 695 // throw is inside a try block, and there is a catch block, 696 // or throw is inside a lambda expression/anonymous class/local class 697 break; 698 } 699 if (ancestor.getType() == TokenTypes.LITERAL_CATCH 700 || ancestor.getType() == TokenTypes.LITERAL_FINALLY) { 701 // if the throw is inside a catch or finally block, 702 // skip the immediate ancestor (try token) 703 ancestor = ancestor.getParent(); 704 } 705 ancestor = ancestor.getParent(); 706 } 707 return ancestor != methodBodyAst; 708 } 709 710 /** 711 * Combine ExceptionInfo collections together by matching names. 712 * 713 * @param first the first collection of ExceptionInfo 714 * @param second the second collection of ExceptionInfo 715 * @return combined list of ExceptionInfo 716 */ 717 private static List<ExceptionInfo> combineExceptionInfo(Collection<ExceptionInfo> first, 718 Iterable<ExceptionInfo> second) { 719 final List<ExceptionInfo> result = new ArrayList<>(first); 720 for (ExceptionInfo exceptionInfo : second) { 721 if (result.stream().noneMatch(item -> isExceptionInfoSame(item, exceptionInfo))) { 722 result.add(exceptionInfo); 723 } 724 } 725 return result; 726 } 727 728 /** 729 * Finds node of specified type among root children, siblings, siblings children 730 * on any deep level. 731 * 732 * @param root DetailAST 733 * @param astType value of TokenType 734 * @return {@link List} of {@link DetailAST} nodes which matches the predicate. 735 */ 736 public static List<DetailAST> findTokensInAstByType(DetailAST root, int astType) { 737 final List<DetailAST> result = new ArrayList<>(); 738 // iterative preorder depth-first search 739 DetailAST curNode = root; 740 do { 741 // process curNode 742 if (curNode.getType() == astType) { 743 result.add(curNode); 744 } 745 // process children (if any) 746 if (curNode.hasChildren()) { 747 curNode = curNode.getFirstChild(); 748 continue; 749 } 750 // backtrack to parent if last child, stopping at root 751 while (curNode != root && curNode.getNextSibling() == null) { 752 curNode = curNode.getParent(); 753 } 754 // explore siblings if not root 755 if (curNode != root) { 756 curNode = curNode.getNextSibling(); 757 } 758 } while (curNode != root); 759 return result; 760 } 761 762 /** 763 * Checks a set of tags for matching parameters. 764 * 765 * @param tags the tags to check 766 * @param parent the node which takes the parameters 767 * @param reportExpectedTags whether we should report if do not find 768 * expected tag 769 */ 770 private void checkParamTags(final List<JavadocTag> tags, 771 final DetailAST parent, boolean reportExpectedTags) { 772 final List<DetailAST> params = getParameters(parent); 773 final List<DetailAST> typeParams = CheckUtil 774 .getTypeParameters(parent); 775 776 // Loop over the tags, checking to see they exist in the params. 777 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 778 while (tagIt.hasNext()) { 779 final JavadocTag tag = tagIt.next(); 780 781 if (!tag.isParamTag()) { 782 continue; 783 } 784 785 tagIt.remove(); 786 787 final String arg1 = tag.getFirstArg(); 788 boolean found = removeMatchingParam(params, arg1); 789 790 if (arg1.startsWith(ELEMENT_START) && arg1.endsWith(ELEMENT_END)) { 791 found = searchMatchingTypeParameter(typeParams, 792 arg1.substring(1, arg1.length() - 1)); 793 } 794 795 // Handle extra JavadocTag 796 if (!found) { 797 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, 798 "@param", arg1); 799 } 800 } 801 802 // Now dump out all type parameters/parameters without tags :- unless 803 // the user has chosen to suppress these problems 804 if (!allowMissingParamTags && reportExpectedTags) { 805 for (DetailAST param : params) { 806 log(param, MSG_EXPECTED_TAG, 807 JavadocTagInfo.PARAM.getText(), param.getText()); 808 } 809 810 for (DetailAST typeParam : typeParams) { 811 log(typeParam, MSG_EXPECTED_TAG, 812 JavadocTagInfo.PARAM.getText(), 813 ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText() 814 + ELEMENT_END); 815 } 816 } 817 } 818 819 /** 820 * Returns true if required type found in type parameters. 821 * 822 * @param typeParams 823 * collection of type parameters 824 * @param requiredTypeName 825 * name of required type 826 * @return true if required type found in type parameters. 827 */ 828 private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams, 829 String requiredTypeName) { 830 // Loop looking for matching type param 831 final Iterator<DetailAST> typeParamsIt = typeParams.iterator(); 832 boolean found = false; 833 while (typeParamsIt.hasNext()) { 834 final DetailAST typeParam = typeParamsIt.next(); 835 if (typeParam.findFirstToken(TokenTypes.IDENT).getText() 836 .equals(requiredTypeName)) { 837 found = true; 838 typeParamsIt.remove(); 839 break; 840 } 841 } 842 return found; 843 } 844 845 /** 846 * Remove parameter from params collection by name. 847 * 848 * @param params collection of DetailAST parameters 849 * @param paramName name of parameter 850 * @return true if parameter found and removed 851 */ 852 private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) { 853 boolean found = false; 854 final Iterator<DetailAST> paramIt = params.iterator(); 855 while (paramIt.hasNext()) { 856 final DetailAST param = paramIt.next(); 857 if (param.getText().equals(paramName)) { 858 found = true; 859 paramIt.remove(); 860 break; 861 } 862 } 863 return found; 864 } 865 866 /** 867 * Checks for only one return tag. All return tags will be removed from the 868 * supplied list. 869 * 870 * @param tags the tags to check 871 * @param lineNo the line number of the expected tag 872 * @param reportExpectedTags whether we should report if do not find 873 * expected tag 874 */ 875 private void checkReturnTag(List<JavadocTag> tags, int lineNo, 876 boolean reportExpectedTags) { 877 // Loop over tags finding return tags. After the first one, report a 878 // violation. 879 boolean found = false; 880 final ListIterator<JavadocTag> it = tags.listIterator(); 881 while (it.hasNext()) { 882 final JavadocTag javadocTag = it.next(); 883 if (javadocTag.isReturnTag()) { 884 if (found) { 885 log(javadocTag.getLineNo(), javadocTag.getColumnNo(), 886 MSG_DUPLICATE_TAG, 887 JavadocTagInfo.RETURN.getText()); 888 } 889 found = true; 890 it.remove(); 891 } 892 } 893 894 // Handle there being no @return tags :- unless 895 // the user has chosen to suppress these problems 896 if (!found && !allowMissingReturnTag && reportExpectedTags) { 897 log(lineNo, MSG_RETURN_EXPECTED); 898 } 899 } 900 901 /** 902 * Checks a set of tags for matching throws. 903 * 904 * @param tags the tags to check 905 * @param throwsList the throws to check 906 * @param reportExpectedTags whether we should report if do not find 907 * expected tag 908 */ 909 private void checkThrowsTags(List<JavadocTag> tags, 910 List<ExceptionInfo> throwsList, boolean reportExpectedTags) { 911 // Loop over the tags, checking to see they exist in the throws. 912 // The foundThrows used for performance only 913 final Set<String> foundThrows = new HashSet<>(); 914 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 915 while (tagIt.hasNext()) { 916 final JavadocTag tag = tagIt.next(); 917 918 if (!tag.isThrowsTag()) { 919 continue; 920 } 921 tagIt.remove(); 922 923 // Loop looking for matching throw 924 final Token token = new Token(tag.getFirstArg(), tag.getLineNo(), tag 925 .getColumnNo()); 926 final ClassInfo documentedClassInfo = new ClassInfo(token); 927 processThrows(throwsList, documentedClassInfo, foundThrows); 928 } 929 // Now dump out all throws without tags :- unless 930 // the user has chosen to suppress these problems 931 if (validateThrows && reportExpectedTags) { 932 throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound()) 933 .forEach(exceptionInfo -> { 934 final Token token = exceptionInfo.getName(); 935 log(exceptionInfo.getAst(), 936 MSG_EXPECTED_TAG, 937 JavadocTagInfo.THROWS.getText(), token.getText()); 938 }); 939 } 940 } 941 942 /** 943 * Verifies that documented exception is in throws. 944 * 945 * @param throwsIterable collection of throws 946 * @param documentedClassInfo documented exception class info 947 * @param foundThrows previously found throws 948 */ 949 private static void processThrows(Iterable<ExceptionInfo> throwsIterable, 950 ClassInfo documentedClassInfo, Set<String> foundThrows) { 951 ExceptionInfo foundException = null; 952 953 // First look for matches on the exception name 954 for (ExceptionInfo exceptionInfo : throwsIterable) { 955 if (isClassNamesSame(exceptionInfo.getName().getText(), 956 documentedClassInfo.getName().getText())) { 957 foundException = exceptionInfo; 958 break; 959 } 960 } 961 962 if (foundException != null) { 963 foundException.setFound(); 964 foundThrows.add(documentedClassInfo.getName().getText()); 965 } 966 } 967 968 /** 969 * Check that ExceptionInfo objects are same by name. 970 * 971 * @param info1 ExceptionInfo object 972 * @param info2 ExceptionInfo object 973 * @return true is ExceptionInfo object have the same name 974 */ 975 private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) { 976 return isClassNamesSame(info1.getName().getText(), 977 info2.getName().getText()); 978 } 979 980 /** 981 * Check that class names are same by short name of class. If some class name is fully 982 * qualified it is cut to short name. 983 * 984 * @param class1 class name 985 * @param class2 class name 986 * @return true is ExceptionInfo object have the same name 987 */ 988 private static boolean isClassNamesSame(String class1, String class2) { 989 boolean result = false; 990 if (class1.equals(class2)) { 991 result = true; 992 } 993 else { 994 final String separator = "."; 995 if (class1.contains(separator) || class2.contains(separator)) { 996 final String class1ShortName = class1 997 .substring(class1.lastIndexOf('.') + 1); 998 final String class2ShortName = class2 999 .substring(class2.lastIndexOf('.') + 1); 1000 result = class1ShortName.equals(class2ShortName); 1001 } 1002 } 1003 return result; 1004 } 1005 1006 /** 1007 * Contains class's {@code Token}. 1008 */ 1009 private static class ClassInfo { 1010 1011 /** {@code FullIdent} associated with this class. */ 1012 private final Token name; 1013 1014 /** 1015 * Creates new instance of class information object. 1016 * 1017 * @param className token which represents class name. 1018 * @throws IllegalArgumentException when className is nulls 1019 */ 1020 protected ClassInfo(final Token className) { 1021 name = className; 1022 } 1023 1024 /** 1025 * Gets class name. 1026 * 1027 * @return class name 1028 */ 1029 public final Token getName() { 1030 return name; 1031 } 1032 1033 } 1034 1035 /** 1036 * Represents text element with location in the text. 1037 */ 1038 private static final class Token { 1039 1040 /** Token's column number. */ 1041 private final int columnNo; 1042 /** Token's line number. */ 1043 private final int lineNo; 1044 /** Token's text. */ 1045 private final String text; 1046 1047 /** 1048 * Creates token. 1049 * 1050 * @param text token's text 1051 * @param lineNo token's line number 1052 * @param columnNo token's column number 1053 */ 1054 private Token(String text, int lineNo, int columnNo) { 1055 this.text = text; 1056 this.lineNo = lineNo; 1057 this.columnNo = columnNo; 1058 } 1059 1060 /** 1061 * Converts FullIdent to Token. 1062 * 1063 * @param fullIdent full ident to convert. 1064 */ 1065 private Token(FullIdent fullIdent) { 1066 text = fullIdent.getText(); 1067 lineNo = fullIdent.getLineNo(); 1068 columnNo = fullIdent.getColumnNo(); 1069 } 1070 1071 /** 1072 * Gets text of the token. 1073 * 1074 * @return text of the token 1075 */ 1076 public String getText() { 1077 return text; 1078 } 1079 1080 @Override 1081 public String toString() { 1082 return "Token[" + text + "(" + lineNo 1083 + "x" + columnNo + ")]"; 1084 } 1085 1086 } 1087 1088 /** Stores useful information about declared exception. */ 1089 private static final class ExceptionInfo { 1090 1091 /** AST node representing this exception. */ 1092 private final DetailAST ast; 1093 1094 /** Class information associated with this exception. */ 1095 private final ClassInfo classInfo; 1096 /** Does the exception have throws tag associated with. */ 1097 private boolean found; 1098 1099 /** 1100 * Creates new instance for {@code FullIdent}. 1101 * 1102 * @param ast AST node representing this exception 1103 * @param classInfo class info 1104 */ 1105 private ExceptionInfo(DetailAST ast, ClassInfo classInfo) { 1106 this.ast = ast; 1107 this.classInfo = classInfo; 1108 } 1109 1110 /** 1111 * Gets the AST node representing this exception. 1112 * 1113 * @return the AST node representing this exception 1114 */ 1115 private DetailAST getAst() { 1116 return ast; 1117 } 1118 1119 /** Mark that the exception has associated throws tag. */ 1120 private void setFound() { 1121 found = true; 1122 } 1123 1124 /** 1125 * Checks that the exception has throws tag associated with it. 1126 * 1127 * @return whether the exception has throws tag associated with 1128 */ 1129 private boolean isFound() { 1130 return found; 1131 } 1132 1133 /** 1134 * Gets exception name. 1135 * 1136 * @return exception's name 1137 */ 1138 private Token getName() { 1139 return classInfo.getName(); 1140 } 1141 1142 } 1143 1144}