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