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