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