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