001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2021 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.Collections; 025import java.util.HashSet; 026import java.util.Iterator; 027import java.util.List; 028import java.util.ListIterator; 029import java.util.Set; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 034import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 035import com.puppycrawl.tools.checkstyle.api.DetailAST; 036import com.puppycrawl.tools.checkstyle.api.FileContents; 037import com.puppycrawl.tools.checkstyle.api.FullIdent; 038import com.puppycrawl.tools.checkstyle.api.TextBlock; 039import com.puppycrawl.tools.checkstyle.api.TokenTypes; 040import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 041import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 042import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 043import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 044 045/** 046 * <p> 047 * Checks the Javadoc of a method or constructor. 048 * </p> 049 * <p> 050 * Violates parameters and type parameters for which no param tags are present can 051 * be suppressed by defining property {@code allowMissingParamTags}. 052 * </p> 053 * <p> 054 * Violates methods which return non-void but for which no return tag is present can 055 * be suppressed by defining property {@code allowMissingReturnTag}. 056 * </p> 057 * <p> 058 * Violates exceptions which are declared to be thrown (by {@code throws} in the method 059 * signature or by {@code throw new} in the method body), but for which no throws tag is 060 * present by activation of property {@code validateThrows}. 061 * Note that {@code throw new} is not checked in the following places: 062 * </p> 063 * <ul> 064 * <li> 065 * Inside a try block (with catch). It is not possible to determine if the thrown 066 * exception can be caught by the catch block as there is no knowledge of the 067 * inheritance hierarchy, so the try block is ignored entirely. However, catch 068 * and finally blocks, as well as try blocks without catch, are still checked. 069 * </li> 070 * <li> 071 * Local classes, anonymous classes and lambda expressions. It is not known when the 072 * throw statements inside such classes are going to be evaluated, so they are ignored. 073 * </li> 074 * </ul> 075 * <p> 076 * ATTENTION: Checkstyle does not have information about hierarchy of exception types 077 * so usage of base class is considered as separate exception type. 078 * As workaround you need to specify both types in javadoc (parent and exact type). 079 * </p> 080 * <p> 081 * Javadoc is not required on a method that is tagged with the {@code @Override} 082 * annotation. However under Java 5 it is not possible to mark a method required 083 * for an interface (this was <i>corrected</i> under Java 6). Hence Checkstyle 084 * supports using the convention of using a single {@code {@inheritDoc}} tag 085 * instead of all the other tags. 086 * </p> 087 * <p> 088 * Note that only inheritable items will allow the {@code {@inheritDoc}} 089 * tag to be used in place of comments. Static methods at all visibilities, 090 * private non-static methods and constructors are not inheritable. 091 * </p> 092 * <p> 093 * For example, if the following method is implementing a method required by 094 * an interface, then the Javadoc could be done as: 095 * </p> 096 * <pre> 097 * /** {@inheritDoc} */ 098 * public int checkReturnTag(final int aTagIndex, 099 * JavadocTag[] aTags, 100 * int aLineNo) 101 * </pre> 102 * <ul> 103 * <li> 104 * Property {@code allowedAnnotations} - Specify the list of annotations 105 * 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 the list of annotations that allow missed documentation. */ 390 private List<String> allowedAnnotations = Collections.singletonList("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 the list of annotations that allow missed documentation. 403 * 404 * @param userAnnotations user's value. 405 */ 406 public void setAllowedAnnotations(String... userAnnotations) { 407 allowedAnnotations = Arrays.asList(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 private void processAST(DetailAST ast) { 506 if (shouldCheck(ast)) { 507 final FileContents contents = getFileContents(); 508 final TextBlock textBlock = contents.getJavadocBefore(ast.getLineNo()); 509 510 if (textBlock != null) { 511 checkComment(ast, textBlock); 512 } 513 } 514 } 515 516 /** 517 * Whether we should check this node. 518 * 519 * @param ast a given node. 520 * @return whether we should check a given node. 521 */ 522 private boolean shouldCheck(final DetailAST ast) { 523 final AccessModifierOption surroundingAccessModifier = CheckUtil 524 .getSurroundingAccessModifier(ast); 525 final AccessModifierOption accessModifier = CheckUtil 526 .getAccessModifierFromModifiersToken(ast); 527 return surroundingAccessModifier != null 528 && Arrays.stream(accessModifiers) 529 .anyMatch(modifier -> modifier == surroundingAccessModifier) 530 && Arrays.stream(accessModifiers).anyMatch(modifier -> modifier == accessModifier); 531 } 532 533 /** 534 * Checks the Javadoc for a method. 535 * 536 * @param ast the token for the method 537 * @param comment the Javadoc comment 538 */ 539 private void checkComment(DetailAST ast, TextBlock comment) { 540 final List<JavadocTag> tags = getMethodTags(comment); 541 542 if (!hasShortCircuitTag(ast, tags)) { 543 if (ast.getType() == TokenTypes.ANNOTATION_FIELD_DEF) { 544 checkReturnTag(tags, ast.getLineNo(), true); 545 } 546 else { 547 final Iterator<JavadocTag> it = tags.iterator(); 548 // Check for inheritDoc 549 boolean hasInheritDocTag = false; 550 while (!hasInheritDocTag && it.hasNext()) { 551 hasInheritDocTag = it.next().isInheritDocTag(); 552 } 553 final boolean reportExpectedTags = !hasInheritDocTag 554 && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations); 555 556 // COMPACT_CTOR_DEF has no parameters 557 if (ast.getType() != TokenTypes.COMPACT_CTOR_DEF) { 558 checkParamTags(tags, ast, reportExpectedTags); 559 } 560 final List<ExceptionInfo> throwed = 561 combineExceptionInfo(getThrows(ast), getThrowed(ast)); 562 checkThrowsTags(tags, throwed, reportExpectedTags); 563 if (CheckUtil.isNonVoidMethod(ast)) { 564 checkReturnTag(tags, ast.getLineNo(), reportExpectedTags); 565 } 566 567 } 568 569 // Dump out all unused tags 570 tags.stream().filter(javadocTag -> !javadocTag.isSeeOrInheritDocTag()) 571 .forEach(javadocTag -> log(javadocTag.getLineNo(), MSG_UNUSED_TAG_GENERAL)); 572 } 573 } 574 575 /** 576 * Validates whether the Javadoc has a short circuit tag. Currently this is 577 * the inheritTag. Any violations are logged. 578 * 579 * @param ast the construct being checked 580 * @param tags the list of Javadoc tags associated with the construct 581 * @return true if the construct has a short circuit tag. 582 */ 583 private boolean hasShortCircuitTag(final DetailAST ast, final List<JavadocTag> tags) { 584 boolean result = true; 585 // Check if it contains {@inheritDoc} tag 586 if (tags.size() == 1 587 && tags.get(0).isInheritDocTag()) { 588 // Invalid if private, a constructor, or a static method 589 if (!JavadocTagInfo.INHERIT_DOC.isValidOn(ast)) { 590 log(ast, MSG_INVALID_INHERIT_DOC); 591 } 592 } 593 else { 594 result = false; 595 } 596 return result; 597 } 598 599 /** 600 * Returns the tags in a javadoc comment. Only finds throws, exception, 601 * param, return and see tags. 602 * 603 * @param comment the Javadoc comment 604 * @return the tags found 605 */ 606 private static List<JavadocTag> getMethodTags(TextBlock comment) { 607 final String[] lines = comment.getText(); 608 final List<JavadocTag> tags = new ArrayList<>(); 609 int currentLine = comment.getStartLineNo() - 1; 610 final int startColumnNumber = comment.getStartColNo(); 611 612 for (int i = 0; i < lines.length; i++) { 613 currentLine++; 614 final Matcher javadocArgMatcher = 615 MATCH_JAVADOC_ARG.matcher(lines[i]); 616 final Matcher javadocArgMissingDescriptionMatcher = 617 MATCH_JAVADOC_ARG_MISSING_DESCRIPTION.matcher(lines[i]); 618 final Matcher javadocNoargMatcher = 619 MATCH_JAVADOC_NOARG.matcher(lines[i]); 620 final Matcher noargCurlyMatcher = 621 MATCH_JAVADOC_NOARG_CURLY.matcher(lines[i]); 622 final Matcher noargMultilineStart = 623 MATCH_JAVADOC_NOARG_MULTILINE_START.matcher(lines[i]); 624 625 if (javadocArgMatcher.find()) { 626 final int col = calculateTagColumn(javadocArgMatcher, i, startColumnNumber); 627 tags.add(new JavadocTag(currentLine, col, javadocArgMatcher.group(1), 628 javadocArgMatcher.group(2))); 629 } 630 else if (javadocArgMissingDescriptionMatcher.find()) { 631 final int col = calculateTagColumn(javadocArgMissingDescriptionMatcher, i, 632 startColumnNumber); 633 tags.add(new JavadocTag(currentLine, col, 634 javadocArgMissingDescriptionMatcher.group(1), 635 javadocArgMissingDescriptionMatcher.group(2))); 636 } 637 else if (javadocNoargMatcher.find()) { 638 final int col = calculateTagColumn(javadocNoargMatcher, i, startColumnNumber); 639 tags.add(new JavadocTag(currentLine, col, javadocNoargMatcher.group(1))); 640 } 641 else if (noargCurlyMatcher.find()) { 642 final int col = calculateTagColumn(noargCurlyMatcher, i, startColumnNumber); 643 tags.add(new JavadocTag(currentLine, col, noargCurlyMatcher.group(1))); 644 } 645 else if (noargMultilineStart.find()) { 646 tags.addAll(getMultilineNoArgTags(noargMultilineStart, lines, i, currentLine)); 647 } 648 } 649 return tags; 650 } 651 652 /** 653 * Calculates column number using Javadoc tag matcher. 654 * 655 * @param javadocTagMatcher found javadoc tag matcher 656 * @param lineNumber line number of Javadoc tag in comment 657 * @param startColumnNumber column number of Javadoc comment beginning 658 * @return column number 659 */ 660 private static int calculateTagColumn(Matcher javadocTagMatcher, 661 int lineNumber, int startColumnNumber) { 662 int col = javadocTagMatcher.start(1) - 1; 663 if (lineNumber == 0) { 664 col += startColumnNumber; 665 } 666 return col; 667 } 668 669 /** 670 * Gets multiline Javadoc tags with no arguments. 671 * 672 * @param noargMultilineStart javadoc tag Matcher 673 * @param lines comment text lines 674 * @param lineIndex line number that contains the javadoc tag 675 * @param tagLine javadoc tag line number in file 676 * @return javadoc tags with no arguments 677 */ 678 private static List<JavadocTag> getMultilineNoArgTags(final Matcher noargMultilineStart, 679 final String[] lines, final int lineIndex, final int tagLine) { 680 int remIndex = lineIndex; 681 Matcher multilineCont; 682 683 do { 684 remIndex++; 685 multilineCont = MATCH_JAVADOC_MULTILINE_CONT.matcher(lines[remIndex]); 686 } while (!multilineCont.find()); 687 688 final List<JavadocTag> tags = new ArrayList<>(); 689 final String lFin = multilineCont.group(1); 690 if (!NEXT_TAG.equals(lFin) 691 && !END_JAVADOC.equals(lFin)) { 692 final String param1 = noargMultilineStart.group(1); 693 final int col = noargMultilineStart.start(1) - 1; 694 695 tags.add(new JavadocTag(tagLine, col, param1)); 696 } 697 698 return tags; 699 } 700 701 /** 702 * Computes the parameter nodes for a method. 703 * 704 * @param ast the method node. 705 * @return the list of parameter nodes for ast. 706 */ 707 private static List<DetailAST> getParameters(DetailAST ast) { 708 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS); 709 final List<DetailAST> returnValue = new ArrayList<>(); 710 711 DetailAST child = params.getFirstChild(); 712 while (child != null) { 713 if (child.getType() == TokenTypes.PARAMETER_DEF) { 714 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT); 715 if (ident != null) { 716 returnValue.add(ident); 717 } 718 } 719 child = child.getNextSibling(); 720 } 721 return returnValue; 722 } 723 724 /** 725 * Computes the exception nodes for a method. 726 * 727 * @param ast the method node. 728 * @return the list of exception nodes for ast. 729 */ 730 private static List<ExceptionInfo> getThrows(DetailAST ast) { 731 final List<ExceptionInfo> returnValue = new ArrayList<>(); 732 final DetailAST throwsAST = ast 733 .findFirstToken(TokenTypes.LITERAL_THROWS); 734 if (throwsAST != null) { 735 DetailAST child = throwsAST.getFirstChild(); 736 while (child != null) { 737 if (child.getType() == TokenTypes.IDENT 738 || child.getType() == TokenTypes.DOT) { 739 returnValue.add(getExceptionInfo(child)); 740 } 741 child = child.getNextSibling(); 742 } 743 } 744 return returnValue; 745 } 746 747 /** 748 * Get ExceptionInfo for all exceptions that throws in method code by 'throw new'. 749 * 750 * @param methodAst method DetailAST object where to find exceptions 751 * @return list of ExceptionInfo 752 */ 753 private static List<ExceptionInfo> getThrowed(DetailAST methodAst) { 754 final List<ExceptionInfo> returnValue = new ArrayList<>(); 755 final DetailAST blockAst = methodAst.findFirstToken(TokenTypes.SLIST); 756 if (blockAst != null) { 757 final List<DetailAST> throwLiterals = findTokensInAstByType(blockAst, 758 TokenTypes.LITERAL_THROW); 759 for (DetailAST throwAst : throwLiterals) { 760 if (!isInIgnoreBlock(blockAst, throwAst)) { 761 final DetailAST newAst = throwAst.getFirstChild().getFirstChild(); 762 if (newAst.getType() == TokenTypes.LITERAL_NEW) { 763 final DetailAST child = newAst.getFirstChild(); 764 returnValue.add(getExceptionInfo(child)); 765 } 766 } 767 } 768 } 769 return returnValue; 770 } 771 772 /** 773 * Get ExceptionInfo instance. 774 * 775 * @param ast DetailAST object where to find exceptions node; 776 * @return ExceptionInfo 777 */ 778 private static ExceptionInfo getExceptionInfo(DetailAST ast) { 779 final FullIdent ident = FullIdent.createFullIdent(ast); 780 final DetailAST firstClassNameNode = getFirstClassNameNode(ast); 781 return new ExceptionInfo(firstClassNameNode, 782 new ClassInfo(new Token(ident))); 783 } 784 785 /** 786 * Get node where class name of exception starts. 787 * 788 * @param ast DetailAST object where to find exceptions node; 789 * @return exception node where class name starts 790 */ 791 private static DetailAST getFirstClassNameNode(DetailAST ast) { 792 DetailAST startNode = ast; 793 while (startNode.getType() == TokenTypes.DOT) { 794 startNode = startNode.getFirstChild(); 795 } 796 return startNode; 797 } 798 799 /** 800 * Checks if a 'throw' usage is contained within a block that should be ignored. 801 * Such blocks consist of try (with catch) blocks, local classes, anonymous classes, 802 * and lambda expressions. Note that a try block without catch is not considered. 803 * 804 * @param methodBodyAst DetailAST node representing the method body 805 * @param throwAst DetailAST node representing the 'throw' literal 806 * @return true if throwAst is inside a block that should be ignored 807 */ 808 private static boolean isInIgnoreBlock(DetailAST methodBodyAst, DetailAST throwAst) { 809 DetailAST ancestor = throwAst.getParent(); 810 while (ancestor != methodBodyAst) { 811 if (ancestor.getType() == TokenTypes.LITERAL_TRY 812 && ancestor.findFirstToken(TokenTypes.LITERAL_CATCH) != null 813 || ancestor.getType() == TokenTypes.LAMBDA 814 || ancestor.getType() == TokenTypes.OBJBLOCK) { 815 // throw is inside a try block, and there is a catch block, 816 // or throw is inside a lambda expression/anonymous class/local class 817 break; 818 } 819 if (ancestor.getType() == TokenTypes.LITERAL_CATCH 820 || ancestor.getType() == TokenTypes.LITERAL_FINALLY) { 821 // if the throw is inside a catch or finally block, 822 // skip the immediate ancestor (try token) 823 ancestor = ancestor.getParent(); 824 } 825 ancestor = ancestor.getParent(); 826 } 827 return ancestor != methodBodyAst; 828 } 829 830 /** 831 * Combine ExceptionInfo lists together by matching names. 832 * 833 * @param list1 list of ExceptionInfo 834 * @param list2 list of ExceptionInfo 835 * @return combined list of ExceptionInfo 836 */ 837 private static List<ExceptionInfo> combineExceptionInfo(List<ExceptionInfo> list1, 838 List<ExceptionInfo> list2) { 839 final List<ExceptionInfo> result = new ArrayList<>(list1); 840 for (ExceptionInfo exceptionInfo : list2) { 841 if (result.stream().noneMatch(item -> isExceptionInfoSame(item, exceptionInfo))) { 842 result.add(exceptionInfo); 843 } 844 } 845 return result; 846 } 847 848 /** 849 * Finds node of specified type among root children, siblings, siblings children 850 * on any deep level. 851 * 852 * @param root DetailAST 853 * @param astType value of TokenType 854 * @return {@link List} of {@link DetailAST} nodes which matches the predicate. 855 */ 856 public static List<DetailAST> findTokensInAstByType(DetailAST root, int astType) { 857 final List<DetailAST> result = new ArrayList<>(); 858 // iterative preorder depth-first search 859 DetailAST curNode = root; 860 do { 861 // process curNode 862 if (curNode.getType() == astType) { 863 result.add(curNode); 864 } 865 // process children (if any) 866 if (curNode.hasChildren()) { 867 curNode = curNode.getFirstChild(); 868 continue; 869 } 870 // backtrack to parent if last child, stopping at root 871 while (curNode != root && curNode.getNextSibling() == null) { 872 curNode = curNode.getParent(); 873 } 874 // explore siblings if not root 875 if (curNode != root) { 876 curNode = curNode.getNextSibling(); 877 } 878 } while (curNode != root); 879 return result; 880 } 881 882 /** 883 * Checks a set of tags for matching parameters. 884 * 885 * @param tags the tags to check 886 * @param parent the node which takes the parameters 887 * @param reportExpectedTags whether we should report if do not find 888 * expected tag 889 */ 890 private void checkParamTags(final List<JavadocTag> tags, 891 final DetailAST parent, boolean reportExpectedTags) { 892 final List<DetailAST> params = getParameters(parent); 893 final List<DetailAST> typeParams = CheckUtil 894 .getTypeParameters(parent); 895 896 // Loop over the tags, checking to see they exist in the params. 897 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 898 while (tagIt.hasNext()) { 899 final JavadocTag tag = tagIt.next(); 900 901 if (!tag.isParamTag()) { 902 continue; 903 } 904 905 tagIt.remove(); 906 907 final String arg1 = tag.getFirstArg(); 908 boolean found = removeMatchingParam(params, arg1); 909 910 if (CommonUtil.startsWithChar(arg1, '<') && CommonUtil.endsWithChar(arg1, '>')) { 911 found = searchMatchingTypeParameter(typeParams, 912 arg1.substring(1, arg1.length() - 1)); 913 } 914 915 // Handle extra JavadocTag 916 if (!found) { 917 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, 918 "@param", arg1); 919 } 920 } 921 922 // Now dump out all type parameters/parameters without tags :- unless 923 // the user has chosen to suppress these problems 924 if (!allowMissingParamTags && reportExpectedTags) { 925 for (DetailAST param : params) { 926 log(param, MSG_EXPECTED_TAG, 927 JavadocTagInfo.PARAM.getText(), param.getText()); 928 } 929 930 for (DetailAST typeParam : typeParams) { 931 log(typeParam, MSG_EXPECTED_TAG, 932 JavadocTagInfo.PARAM.getText(), 933 "<" + typeParam.findFirstToken(TokenTypes.IDENT).getText() 934 + ">"); 935 } 936 } 937 } 938 939 /** 940 * Returns true if required type found in type parameters. 941 * 942 * @param typeParams 943 * list of type parameters 944 * @param requiredTypeName 945 * name of required type 946 * @return true if required type found in type parameters. 947 */ 948 private static boolean searchMatchingTypeParameter(List<DetailAST> typeParams, 949 String requiredTypeName) { 950 // Loop looking for matching type param 951 final Iterator<DetailAST> typeParamsIt = typeParams.iterator(); 952 boolean found = false; 953 while (typeParamsIt.hasNext()) { 954 final DetailAST typeParam = typeParamsIt.next(); 955 if (typeParam.findFirstToken(TokenTypes.IDENT).getText() 956 .equals(requiredTypeName)) { 957 found = true; 958 typeParamsIt.remove(); 959 break; 960 } 961 } 962 return found; 963 } 964 965 /** 966 * Remove parameter from params collection by name. 967 * 968 * @param params collection of DetailAST parameters 969 * @param paramName name of parameter 970 * @return true if parameter found and removed 971 */ 972 private static boolean removeMatchingParam(List<DetailAST> params, String paramName) { 973 boolean found = false; 974 final Iterator<DetailAST> paramIt = params.iterator(); 975 while (paramIt.hasNext()) { 976 final DetailAST param = paramIt.next(); 977 if (param.getText().equals(paramName)) { 978 found = true; 979 paramIt.remove(); 980 break; 981 } 982 } 983 return found; 984 } 985 986 /** 987 * Checks for only one return tag. All return tags will be removed from the 988 * supplied list. 989 * 990 * @param tags the tags to check 991 * @param lineNo the line number of the expected tag 992 * @param reportExpectedTags whether we should report if do not find 993 * expected tag 994 */ 995 private void checkReturnTag(List<JavadocTag> tags, int lineNo, 996 boolean reportExpectedTags) { 997 // Loop over tags finding return tags. After the first one, report an 998 // violation. 999 boolean found = false; 1000 final ListIterator<JavadocTag> it = tags.listIterator(); 1001 while (it.hasNext()) { 1002 final JavadocTag javadocTag = it.next(); 1003 if (javadocTag.isReturnTag()) { 1004 if (found) { 1005 log(javadocTag.getLineNo(), javadocTag.getColumnNo(), 1006 MSG_DUPLICATE_TAG, 1007 JavadocTagInfo.RETURN.getText()); 1008 } 1009 found = true; 1010 it.remove(); 1011 } 1012 } 1013 1014 // Handle there being no @return tags :- unless 1015 // the user has chosen to suppress these problems 1016 if (!found && !allowMissingReturnTag && reportExpectedTags) { 1017 log(lineNo, MSG_RETURN_EXPECTED); 1018 } 1019 } 1020 1021 /** 1022 * Checks a set of tags for matching throws. 1023 * 1024 * @param tags the tags to check 1025 * @param throwsList the throws to check 1026 * @param reportExpectedTags whether we should report if do not find 1027 * expected tag 1028 */ 1029 private void checkThrowsTags(List<JavadocTag> tags, 1030 List<ExceptionInfo> throwsList, boolean reportExpectedTags) { 1031 // Loop over the tags, checking to see they exist in the throws. 1032 // The foundThrows used for performance only 1033 final Set<String> foundThrows = new HashSet<>(); 1034 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 1035 while (tagIt.hasNext()) { 1036 final JavadocTag tag = tagIt.next(); 1037 1038 if (!tag.isThrowsTag()) { 1039 continue; 1040 } 1041 tagIt.remove(); 1042 1043 // Loop looking for matching throw 1044 final Token token = new Token(tag.getFirstArg(), tag.getLineNo(), tag 1045 .getColumnNo()); 1046 final ClassInfo documentedClassInfo = new ClassInfo(token); 1047 processThrows(throwsList, documentedClassInfo, foundThrows); 1048 } 1049 // Now dump out all throws without tags :- unless 1050 // the user has chosen to suppress these problems 1051 if (validateThrows && reportExpectedTags) { 1052 throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound()) 1053 .forEach(exceptionInfo -> { 1054 final Token token = exceptionInfo.getName(); 1055 log(exceptionInfo.getAst(), 1056 MSG_EXPECTED_TAG, 1057 JavadocTagInfo.THROWS.getText(), token.getText()); 1058 }); 1059 } 1060 } 1061 1062 /** 1063 * Verifies that documented exception is in throws. 1064 * 1065 * @param throwsList list of throws 1066 * @param documentedClassInfo documented exception class info 1067 * @param foundThrows previously found throws 1068 */ 1069 private static void processThrows(List<ExceptionInfo> throwsList, 1070 ClassInfo documentedClassInfo, Set<String> foundThrows) { 1071 ExceptionInfo foundException = null; 1072 1073 // First look for matches on the exception name 1074 for (ExceptionInfo exceptionInfo : throwsList) { 1075 if (isClassNamesSame(exceptionInfo.getName().getText(), 1076 documentedClassInfo.getName().getText())) { 1077 foundException = exceptionInfo; 1078 break; 1079 } 1080 } 1081 1082 if (foundException != null) { 1083 foundException.setFound(); 1084 foundThrows.add(documentedClassInfo.getName().getText()); 1085 } 1086 } 1087 1088 /** 1089 * Check that ExceptionInfo objects are same by name. 1090 * 1091 * @param info1 ExceptionInfo object 1092 * @param info2 ExceptionInfo object 1093 * @return true is ExceptionInfo object have the same name 1094 */ 1095 private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) { 1096 return isClassNamesSame(info1.getName().getText(), 1097 info2.getName().getText()); 1098 } 1099 1100 /** 1101 * Check that class names are same by short name of class. If some class name is fully 1102 * qualified it is cut to short name. 1103 * 1104 * @param class1 class name 1105 * @param class2 class name 1106 * @return true is ExceptionInfo object have the same name 1107 */ 1108 private static boolean isClassNamesSame(String class1, String class2) { 1109 boolean result = false; 1110 if (class1.equals(class2)) { 1111 result = true; 1112 } 1113 else { 1114 final String separator = "."; 1115 if (class1.contains(separator) || class2.contains(separator)) { 1116 final String class1ShortName = class1 1117 .substring(class1.lastIndexOf('.') + 1); 1118 final String class2ShortName = class2 1119 .substring(class2.lastIndexOf('.') + 1); 1120 result = class1ShortName.equals(class2ShortName); 1121 } 1122 } 1123 return result; 1124 } 1125 1126 /** 1127 * Processes class definition. 1128 * 1129 * @param ast class definition to process. 1130 */ 1131 private void processClass(DetailAST ast) { 1132 final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT); 1133 String innerClass = ident.getText(); 1134 1135 innerClass = "$" + innerClass; 1136 currentClassName += innerClass; 1137 } 1138 1139 /** 1140 * Contains class's {@code Token}. 1141 */ 1142 private static class ClassInfo { 1143 1144 /** {@code FullIdent} associated with this class. */ 1145 private final Token name; 1146 1147 /** 1148 * Creates new instance of class information object. 1149 * 1150 * @param className token which represents class name. 1151 * @throws IllegalArgumentException when className is nulls 1152 */ 1153 protected ClassInfo(final Token className) { 1154 name = className; 1155 } 1156 1157 /** 1158 * Gets class name. 1159 * 1160 * @return class name 1161 */ 1162 public final Token getName() { 1163 return name; 1164 } 1165 1166 } 1167 1168 /** 1169 * Represents text element with location in the text. 1170 */ 1171 private static class Token { 1172 1173 /** Token's column number. */ 1174 private final int columnNo; 1175 /** Token's line number. */ 1176 private final int lineNo; 1177 /** Token's text. */ 1178 private final String text; 1179 1180 /** 1181 * Creates token. 1182 * 1183 * @param text token's text 1184 * @param lineNo token's line number 1185 * @param columnNo token's column number 1186 */ 1187 /* package */ Token(String text, int lineNo, int columnNo) { 1188 this.text = text; 1189 this.lineNo = lineNo; 1190 this.columnNo = columnNo; 1191 } 1192 1193 /** 1194 * Converts FullIdent to Token. 1195 * 1196 * @param fullIdent full ident to convert. 1197 */ 1198 /* package */ Token(FullIdent fullIdent) { 1199 text = fullIdent.getText(); 1200 lineNo = fullIdent.getLineNo(); 1201 columnNo = fullIdent.getColumnNo(); 1202 } 1203 1204 /** 1205 * Gets text of the token. 1206 * 1207 * @return text of the token 1208 */ 1209 public String getText() { 1210 return text; 1211 } 1212 1213 @Override 1214 public String toString() { 1215 return "Token[" + text + "(" + lineNo 1216 + "x" + columnNo + ")]"; 1217 } 1218 1219 } 1220 1221 /** Stores useful information about declared exception. */ 1222 private static class ExceptionInfo { 1223 1224 /** AST node representing this exception. */ 1225 private final DetailAST ast; 1226 1227 /** Class information associated with this exception. */ 1228 private final ClassInfo classInfo; 1229 /** Does the exception have throws tag associated with. */ 1230 private boolean found; 1231 1232 /** 1233 * Creates new instance for {@code FullIdent}. 1234 * 1235 * @param ast AST node representing this exception 1236 * @param classInfo class info 1237 */ 1238 /* package */ ExceptionInfo(DetailAST ast, ClassInfo classInfo) { 1239 this.ast = ast; 1240 this.classInfo = classInfo; 1241 } 1242 1243 /** 1244 * Gets the AST node representing this exception. 1245 * 1246 * @return the AST node representing this exception 1247 */ 1248 private DetailAST getAst() { 1249 return ast; 1250 } 1251 1252 /** Mark that the exception has associated throws tag. */ 1253 private void setFound() { 1254 found = true; 1255 } 1256 1257 /** 1258 * Checks that the exception has throws tag associated with it. 1259 * 1260 * @return whether the exception has throws tag associated with 1261 */ 1262 private boolean isFound() { 1263 return found; 1264 } 1265 1266 /** 1267 * Gets exception name. 1268 * 1269 * @return exception's name 1270 */ 1271 private Token getName() { 1272 return classInfo.getName(); 1273 } 1274 1275 } 1276 1277}