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