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