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