001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.javadoc; 021 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.Iterator; 026import java.util.List; 027import java.util.ListIterator; 028import java.util.Optional; 029import java.util.Set; 030import java.util.regex.MatchResult; 031import java.util.regex.Matcher; 032import java.util.regex.Pattern; 033 034import com.puppycrawl.tools.checkstyle.StatelessCheck; 035import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 036import com.puppycrawl.tools.checkstyle.api.Comment; 037import com.puppycrawl.tools.checkstyle.api.DetailAST; 038import com.puppycrawl.tools.checkstyle.api.FullIdent; 039import com.puppycrawl.tools.checkstyle.api.TextBlock; 040import com.puppycrawl.tools.checkstyle.api.TokenTypes; 041import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 042import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; 043import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 044import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 045import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 046import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil; 047 048/** 049 * <div> 050 * Checks the Javadoc of a method or constructor. 051 * </div> 052 * 053 * <p> 054 * Violates parameters and type parameters for which no param tags are present can 055 * be suppressed by defining property {@code allowMissingParamTags}. 056 * </p> 057 * 058 * <p> 059 * Violates methods which return non-void but for which no return tag is present can 060 * be suppressed by defining property {@code allowMissingReturnTag}. 061 * </p> 062 * 063 * <p> 064 * Violates exceptions which are declared to be thrown (by {@code throws} in the method 065 * signature or by {@code throw new} in the method body), but for which no throws tag is 066 * present by activation of property {@code validateThrows}. 067 * Note that {@code throw new} is not checked in the following places: 068 * </p> 069 * <ul> 070 * <li> 071 * Inside a try block (with catch). It is not possible to determine if the thrown 072 * exception can be caught by the catch block as there is no knowledge of the 073 * inheritance hierarchy, so the try block is ignored entirely. However, catch 074 * and finally blocks, as well as try blocks without catch, are still checked. 075 * </li> 076 * <li> 077 * Local classes, anonymous classes and lambda expressions. It is not known when the 078 * throw statements inside such classes are going to be evaluated, so they are ignored. 079 * </li> 080 * </ul> 081 * 082 * <p> 083 * ATTENTION: Checkstyle does not have information about hierarchy of exception types 084 * so usage of base class is considered as separate exception type. 085 * As workaround, you need to specify both types in javadoc (parent and exact type). 086 * </p> 087 * 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 * 096 * <p> 097 * Note that only inheritable items will allow the {@code {@inheritDoc}} 098 * tag to be used in place of comments. Static methods at all visibilities, 099 * private non-static methods and constructors are not inheritable. 100 * </p> 101 * 102 * <p> 103 * For example, if the following method is implementing a method required by 104 * an interface, then the Javadoc could be done as: 105 * </p> 106 * <div class="wrapper"><pre class="prettyprint"><code class="language-java"> 107 * /** {@inheritDoc} */ 108 * public int checkReturnTag(final int aTagIndex, 109 * JavadocTag[] aTags, 110 * int aLineNo) 111 * </code></pre></div> 112 * 113 * @since 3.0 114 */ 115@StatelessCheck 116public class JavadocMethodCheck extends AbstractCheck { 117 118 /** 119 * A key is pointing to the warning message text in "messages.properties" 120 * file. 121 */ 122 public static final String MSG_CLASS_INFO = "javadoc.classInfo"; 123 124 /** 125 * A key is pointing to the warning message text in "messages.properties" 126 * file. 127 */ 128 public static final String MSG_UNUSED_TAG_GENERAL = "javadoc.unusedTagGeneral"; 129 130 /** 131 * A key is pointing to the warning message text in "messages.properties" 132 * file. 133 */ 134 public static final String MSG_INVALID_INHERIT_DOC = "javadoc.invalidInheritDoc"; 135 136 /** 137 * A key is pointing to the warning message text in "messages.properties" 138 * file. 139 */ 140 public static final String MSG_UNUSED_TAG = "javadoc.unusedTag"; 141 142 /** 143 * A key is pointing to the warning message text in "messages.properties" 144 * file. 145 */ 146 public static final String MSG_EXPECTED_TAG = "javadoc.expectedTag"; 147 148 /** 149 * A key is pointing to the warning message text in "messages.properties" 150 * file. 151 */ 152 public static final String MSG_RETURN_EXPECTED = "javadoc.return.expected"; 153 154 /** 155 * A key is pointing to the warning message text in "messages.properties" 156 * file. 157 */ 158 public static final String MSG_DUPLICATE_TAG = "javadoc.duplicateTag"; 159 160 /** Html element start symbol. */ 161 private static final String ELEMENT_START = "<"; 162 163 /** Html element end symbol. */ 164 private static final String ELEMENT_END = ">"; 165 166 /** Compiled regexp to match Javadoc tags that take an argument. */ 167 private static final Pattern MATCH_JAVADOC_ARG = CommonUtil.createPattern( 168 "^\\s*(?>\\*|\\/\\*\\*)?\\s*@(throws|exception|param)\\s+(\\S+)\\s+\\S*"); 169 /** Compiled regexp to match Javadoc tags with argument but with missing description. */ 170 private static final Pattern MATCH_JAVADOC_ARG_MISSING_DESCRIPTION = 171 CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(throws|exception|param)\\s+" 172 + "(\\S[^*]*)(?:(\\s+|\\*\\/))?"); 173 174 /** Compiled regexp to look for a continuation of the comment. */ 175 private static final Pattern MATCH_JAVADOC_MULTILINE_CONT = 176 CommonUtil.createPattern("(\\*\\/|@|[^\\s\\*])"); 177 178 /** Multiline finished at end of comment. */ 179 private static final String END_JAVADOC = "*/"; 180 /** Multiline finished at next Javadoc. */ 181 private static final String NEXT_TAG = "@"; 182 183 /** Compiled regexp to match Javadoc tags with no argument. */ 184 private static final Pattern MATCH_JAVADOC_NOARG = 185 CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(return|see)\\s+\\S"); 186 /** Compiled regexp to match Javadoc tags with no argument allowing inline return tag. */ 187 private static final Pattern MATCH_JAVADOC_NOARG_INLINE_RETURN = 188 CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*\\{?@(return|see)\\s+\\S"); 189 /** Compiled regexp to match first part of multilineJavadoc tags. */ 190 private static final Pattern MATCH_JAVADOC_NOARG_MULTILINE_START = 191 CommonUtil.createPattern("^\\s*(?>\\*|\\/\\*\\*)?\\s*@(return|see)\\s*$"); 192 /** Compiled regexp to match Javadoc tags with no argument and {}. */ 193 private static final Pattern MATCH_JAVADOC_NOARG_CURLY = 194 CommonUtil.createPattern("\\{\\s*@(inheritDoc)\\s*\\}"); 195 196 /** 197 * Control whether to allow inline return tags. 198 */ 199 private boolean allowInlineReturn; 200 201 /** Specify the access modifiers where Javadoc comments are checked. */ 202 private AccessModifierOption[] accessModifiers = { 203 AccessModifierOption.PUBLIC, 204 AccessModifierOption.PROTECTED, 205 AccessModifierOption.PACKAGE, 206 AccessModifierOption.PRIVATE, 207 }; 208 209 /** 210 * Control whether to validate {@code throws} tags. 211 */ 212 private boolean validateThrows; 213 214 /** 215 * Control whether to ignore violations when a method has parameters but does 216 * not have matching {@code param} tags in the javadoc. 217 */ 218 private boolean allowMissingParamTags; 219 220 /** 221 * Control whether to ignore violations when a method returns non-void type 222 * and does not have a {@code return} tag in the javadoc. 223 */ 224 private boolean allowMissingReturnTag; 225 226 /** Specify annotations that allow missed documentation. */ 227 private Set<String> allowedAnnotations = Set.of("Override"); 228 229 /** 230 * Setter to control whether to allow inline return tags. 231 * 232 * @param value a {@code boolean} value 233 * @since 10.23.0 234 */ 235 public void setAllowInlineReturn(boolean value) { 236 allowInlineReturn = value; 237 } 238 239 /** 240 * Setter to control whether to validate {@code throws} tags. 241 * 242 * @param value user's value. 243 * @since 6.0 244 */ 245 public void setValidateThrows(boolean value) { 246 validateThrows = value; 247 } 248 249 /** 250 * Setter to specify annotations that allow missed documentation. 251 * 252 * @param userAnnotations user's value. 253 * @since 6.0 254 */ 255 public void setAllowedAnnotations(String... userAnnotations) { 256 allowedAnnotations = Set.of(userAnnotations); 257 } 258 259 /** 260 * Setter to specify the access modifiers where Javadoc comments are checked. 261 * 262 * @param accessModifiers access modifiers. 263 * @since 8.42 264 */ 265 public void setAccessModifiers(AccessModifierOption... accessModifiers) { 266 this.accessModifiers = 267 UnmodifiableCollectionUtil.copyOfArray(accessModifiers, accessModifiers.length); 268 } 269 270 /** 271 * Setter to control whether to ignore violations when a method has parameters 272 * but does not have matching {@code param} tags in the javadoc. 273 * 274 * @param flag a {@code Boolean} value 275 * @since 3.1 276 */ 277 public void setAllowMissingParamTags(boolean flag) { 278 allowMissingParamTags = flag; 279 } 280 281 /** 282 * Setter to control whether to ignore violations when a method returns non-void type 283 * and does not have a {@code return} tag in the javadoc. 284 * 285 * @param flag a {@code Boolean} value 286 * @since 3.1 287 */ 288 public void setAllowMissingReturnTag(boolean flag) { 289 allowMissingReturnTag = flag; 290 } 291 292 @Override 293 public final int[] getRequiredTokens() { 294 return CommonUtil.EMPTY_INT_ARRAY; 295 } 296 297 @Override 298 public int[] getDefaultTokens() { 299 return getAcceptableTokens(); 300 } 301 302 @Override 303 public int[] getAcceptableTokens() { 304 return new int[] { 305 TokenTypes.METHOD_DEF, 306 TokenTypes.CTOR_DEF, 307 TokenTypes.ANNOTATION_FIELD_DEF, 308 TokenTypes.COMPACT_CTOR_DEF, 309 }; 310 } 311 312 @Override 313 public boolean isCommentNodesRequired() { 314 return true; 315 } 316 317 @Override 318 public final void visitToken(DetailAST ast) { 319 processAST(ast); 320 } 321 322 /** 323 * Called to process an AST when visiting it. 324 * 325 * @param ast the AST to process. Guaranteed to not be PACKAGE_DEF or 326 * IMPORT tokens. 327 */ 328 private void processAST(DetailAST ast) { 329 if (shouldCheck(ast)) { 330 final DetailAST javadocNode = findJavadocComment(ast); 331 332 if (javadocNode != null) { 333 final TextBlock textBlock = convertToTextBlock(javadocNode); 334 checkComment(ast, textBlock); 335 } 336 } 337 } 338 339 /** 340 * Finds the javadoc comment for a given constructor/method AST node. 341 * It looks for javadoc comments immediately before the constructor/method and uses 342 * the last (nearest) one found. If there are multiple consecutive javadoc comments, 343 * the one closest to the method/constructor is used. 344 * If no javadoc is found before the constructor/method, it looks inside the 345 * MODIFIERS node and its children (including annotations) for a javadoc, returning 346 * the last non-empty javadoc if found, or the last javadoc if all are empty. 347 * 348 * @param ast the constructor/method AST node 349 * @return the javadoc comment AST node, or null if not found 350 */ 351 private static DetailAST findJavadocComment(DetailAST ast) { 352 DetailAST javadoc = ast.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN); 353 if (javadoc == null || !JavadocUtil.isJavadocComment(javadoc)) { 354 javadoc = null; 355 } 356 357 if (javadoc == null) { 358 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); 359 javadoc = findJavadocInModifiers(modifiers); 360 if (javadoc == null) { 361 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE); 362 if (type != null) { 363 javadoc = type.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN); 364 } 365 } 366 } 367 368 return javadoc; 369 } 370 371 /** 372 * Finds javadoc comment in MODIFIERS node and its children (including annotations). 373 * Returns the last (nearest) javadoc comment found. 374 * 375 * @param modifiers the MODIFIERS node 376 * @return the javadoc comment node, or null if not found 377 */ 378 private static DetailAST findJavadocInModifiers(DetailAST modifiers) { 379 DetailAST lastJavadoc = null; 380 381 DetailAST child = modifiers.getFirstChild(); 382 while (child != null) { 383 if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN 384 && JavadocUtil.isJavadocComment(child)) { 385 lastJavadoc = child; 386 } 387 else { 388 final DetailAST comment = child.findFirstToken(TokenTypes.BLOCK_COMMENT_BEGIN); 389 if (comment != null && JavadocUtil.isJavadocComment(comment)) { 390 lastJavadoc = comment; 391 } 392 } 393 child = child.getNextSibling(); 394 } 395 return lastJavadoc; 396 } 397 398 /** 399 * Converts a javadoc comment AST node to a TextBlock. 400 * 401 * @param javadocNode the javadoc comment AST node 402 * @return the TextBlock representation 403 */ 404 private TextBlock convertToTextBlock(DetailAST javadocNode) { 405 final int startLineNo = javadocNode.getLineNo(); 406 final DetailAST endNode = javadocNode.getLastChild(); 407 final int endLineNo = endNode.getLineNo(); 408 final int endColNo = endNode.getColumnNo() + 1; 409 final String[] text = extractCommentText(startLineNo, 410 javadocNode.getColumnNo(), endLineNo, endColNo); 411 return new Comment(text, javadocNode.getColumnNo(), endLineNo, endColNo); 412 } 413 414 /** 415 * Extracts comment text from the file. 416 * 417 * @param startLineNo starting line number (1-based) 418 * @param startColNo starting column number (0-based) 419 * @param endLineNo ending line number (1-based) 420 * @param endColNo ending column number (0-based) 421 * @return array of strings representing the comment text 422 * @throws IllegalStateException if extracted text does not start with javadoc marker 423 */ 424 private String[] extractCommentText(int startLineNo, int startColNo, 425 int endLineNo, int endColNo) { 426 final String[] lines; 427 final String firstLine = getLine(startLineNo - 1); 428 429 if (startLineNo == endLineNo) { 430 lines = new String[1]; 431 lines[0] = firstLine.substring(startColNo, endColNo + 1); 432 } 433 else { 434 lines = new String[endLineNo - startLineNo + 1]; 435 lines[0] = firstLine.substring(startColNo); 436 for (int cur = startLineNo; cur < endLineNo; cur++) { 437 lines[cur - startLineNo + 1] = getLine(cur); 438 } 439 final String lastLine = getLine(endLineNo - 1); 440 lines[lines.length - 1] = lastLine; 441 } 442 443 return lines; 444 } 445 446 /** 447 * Whether we should check this node. 448 * 449 * @param ast a given node. 450 * @return whether we should check a given node. 451 */ 452 private boolean shouldCheck(final DetailAST ast) { 453 final Optional<AccessModifierOption> surroundingAccessModifier = CheckUtil 454 .getSurroundingAccessModifier(ast); 455 final AccessModifierOption accessModifier = CheckUtil 456 .getAccessModifierFromModifiersToken(ast); 457 return surroundingAccessModifier.isPresent() && Arrays.stream(accessModifiers) 458 .anyMatch(modifier -> modifier == surroundingAccessModifier.get()) 459 && Arrays.stream(accessModifiers).anyMatch(modifier -> modifier == accessModifier); 460 } 461 462 /** 463 * Checks the Javadoc for a method. 464 * 465 * @param ast the token for the method 466 * @param comment the Javadoc comment 467 */ 468 private void checkComment(DetailAST ast, TextBlock comment) { 469 final List<JavadocTag> tags = getMethodTags(comment); 470 471 if (!hasShortCircuitTag(ast, tags)) { 472 if (ast.getType() == TokenTypes.ANNOTATION_FIELD_DEF) { 473 checkReturnTag(tags, ast.getLineNo(), true); 474 } 475 else { 476 final Iterator<JavadocTag> it = tags.iterator(); 477 // Check for inheritDoc 478 boolean hasInheritDocTag = false; 479 while (!hasInheritDocTag && it.hasNext()) { 480 hasInheritDocTag = it.next().isInheritDocTag(); 481 } 482 final boolean reportExpectedTags = !hasInheritDocTag 483 && !AnnotationUtil.containsAnnotation(ast, allowedAnnotations); 484 485 // COMPACT_CTOR_DEF has no parameters 486 if (ast.getType() == TokenTypes.COMPACT_CTOR_DEF) { 487 checkRecordParamTags(tags, ast, reportExpectedTags); 488 } 489 else { 490 checkParamTags(tags, ast, reportExpectedTags); 491 } 492 final List<ExceptionInfo> throwed = 493 combineExceptionInfo(getThrows(ast), getThrowed(ast)); 494 checkThrowsTags(tags, throwed, reportExpectedTags); 495 if (CheckUtil.isNonVoidMethod(ast)) { 496 checkReturnTag(tags, ast.getLineNo(), reportExpectedTags); 497 } 498 } 499 } 500 tags.stream().filter(javadocTag -> !javadocTag.isSeeOrInheritDocTag()) 501 .forEach(javadocTag -> log(javadocTag.getLineNo(), MSG_UNUSED_TAG_GENERAL)); 502 } 503 504 /** 505 * Retrieves the list of record components from a given record definition. 506 * 507 * @param recordDef the AST node representing the record definition 508 * @return a list of AST nodes representing the record components 509 */ 510 private static List<DetailAST> getRecordComponents(final DetailAST recordDef) { 511 final List<DetailAST> components = new ArrayList<>(); 512 final DetailAST recordDecl = recordDef.findFirstToken(TokenTypes.RECORD_COMPONENTS); 513 514 DetailAST child = recordDecl.getFirstChild(); 515 while (child != null) { 516 if (child.getType() == TokenTypes.RECORD_COMPONENT_DEF) { 517 components.add(child.findFirstToken(TokenTypes.IDENT)); 518 } 519 child = child.getNextSibling(); 520 } 521 return components; 522 } 523 524 /** 525 * Finds the nearest ancestor record definition node for the given AST node. 526 * 527 * @param ast the AST node to start searching from 528 * @return the nearest {@code RECORD_DEF} AST node, or {@code null} if not found 529 */ 530 private static DetailAST getRecordDef(DetailAST ast) { 531 DetailAST current = ast; 532 while (current.getType() != TokenTypes.RECORD_DEF) { 533 current = current.getParent(); 534 } 535 return current; 536 } 537 538 /** 539 * Validates whether the Javadoc has a short circuit tag. Currently, this is 540 * the inheritTag. Any violations are logged. 541 * 542 * @param ast the construct being checked 543 * @param tags the list of Javadoc tags associated with the construct 544 * @return true if the construct has a short circuit tag. 545 */ 546 private boolean hasShortCircuitTag(final DetailAST ast, final List<JavadocTag> tags) { 547 boolean result = true; 548 // Check if it contains {@inheritDoc} tag 549 if (tags.size() == 1 550 && tags.getFirst().isInheritDocTag()) { 551 // Invalid if private, a constructor, or a static method 552 if (!JavadocTagInfo.INHERIT_DOC.isValidOn(ast)) { 553 log(ast, MSG_INVALID_INHERIT_DOC); 554 } 555 } 556 else { 557 result = false; 558 } 559 return result; 560 } 561 562 /** 563 * Returns the tags in a javadoc comment. Only finds throws, exception, 564 * param, return and see tags. 565 * 566 * @param comment the Javadoc comment 567 * @return the tags found 568 */ 569 private List<JavadocTag> getMethodTags(TextBlock comment) { 570 Pattern matchJavadocNoArg = MATCH_JAVADOC_NOARG; 571 if (allowInlineReturn) { 572 matchJavadocNoArg = MATCH_JAVADOC_NOARG_INLINE_RETURN; 573 } 574 final String[] lines = comment.getText(); 575 final List<JavadocTag> tags = new ArrayList<>(); 576 int currentLine = comment.getStartLineNo() - 1; 577 final int startColumnNumber = comment.getStartColNo(); 578 579 for (int i = 0; i < lines.length; i++) { 580 currentLine++; 581 final Matcher javadocArgMatcher = 582 MATCH_JAVADOC_ARG.matcher(lines[i]); 583 final Matcher javadocArgMissingDescriptionMatcher = 584 MATCH_JAVADOC_ARG_MISSING_DESCRIPTION.matcher(lines[i]); 585 final Matcher javadocNoargMatcher = 586 matchJavadocNoArg.matcher(lines[i]); 587 final Matcher noargCurlyMatcher = 588 MATCH_JAVADOC_NOARG_CURLY.matcher(lines[i]); 589 final Matcher noargMultilineStart = 590 MATCH_JAVADOC_NOARG_MULTILINE_START.matcher(lines[i]); 591 592 if (javadocArgMatcher.find()) { 593 final int col = calculateTagColumn(javadocArgMatcher, i, startColumnNumber); 594 tags.add(new JavadocTag(currentLine, col, javadocArgMatcher.group(1), 595 javadocArgMatcher.group(2))); 596 } 597 else if (javadocArgMissingDescriptionMatcher.find()) { 598 final int col = calculateTagColumn(javadocArgMissingDescriptionMatcher, i, 599 startColumnNumber); 600 tags.add(new JavadocTag(currentLine, col, 601 javadocArgMissingDescriptionMatcher.group(1), 602 javadocArgMissingDescriptionMatcher.group(2))); 603 } 604 else if (javadocNoargMatcher.find()) { 605 final int col = calculateTagColumn(javadocNoargMatcher, i, startColumnNumber); 606 tags.add(new JavadocTag(currentLine, col, javadocNoargMatcher.group(1))); 607 } 608 else if (noargCurlyMatcher.find()) { 609 tags.add(new JavadocTag(currentLine, 0, noargCurlyMatcher.group(1))); 610 } 611 else if (noargMultilineStart.find()) { 612 tags.addAll(getMultilineNoArgTags(noargMultilineStart, lines, i, currentLine)); 613 } 614 } 615 return tags; 616 } 617 618 /** 619 * Calculates column number using Javadoc tag matcher. 620 * 621 * @param javadocTagMatchResult found javadoc tag match result 622 * @param lineNumber line number of Javadoc tag in comment 623 * @param startColumnNumber column number of Javadoc comment beginning 624 * @return column number 625 */ 626 private static int calculateTagColumn(MatchResult javadocTagMatchResult, 627 int lineNumber, int startColumnNumber) { 628 int col = javadocTagMatchResult.start(1) - 1; 629 if (lineNumber == 0) { 630 col += startColumnNumber; 631 } 632 return col; 633 } 634 635 /** 636 * Gets multiline Javadoc tags with no arguments. 637 * 638 * @param noargMultilineStart javadoc tag Matcher 639 * @param lines comment text lines 640 * @param lineIndex line number that contains the javadoc tag 641 * @param tagLine javadoc tag line number in file 642 * @return javadoc tags with no arguments 643 */ 644 private static List<JavadocTag> getMultilineNoArgTags(final Matcher noargMultilineStart, 645 final String[] lines, final int lineIndex, final int tagLine) { 646 int remIndex = lineIndex; 647 Matcher multilineCont; 648 649 do { 650 remIndex++; 651 multilineCont = MATCH_JAVADOC_MULTILINE_CONT.matcher(lines[remIndex]); 652 } while (!multilineCont.find()); 653 654 final List<JavadocTag> tags = new ArrayList<>(); 655 final String lFin = multilineCont.group(1); 656 if (!NEXT_TAG.equals(lFin) 657 && !END_JAVADOC.equals(lFin)) { 658 final String param1 = noargMultilineStart.group(1); 659 final int col = noargMultilineStart.start(1) - 1; 660 661 tags.add(new JavadocTag(tagLine, col, param1)); 662 } 663 664 return tags; 665 } 666 667 /** 668 * Computes the parameter nodes for a method. 669 * 670 * @param ast the method node. 671 * @return the list of parameter nodes for ast. 672 */ 673 private static List<DetailAST> getParameters(DetailAST ast) { 674 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS); 675 final List<DetailAST> returnValue = new ArrayList<>(); 676 677 DetailAST child = params.getFirstChild(); 678 while (child != null) { 679 final DetailAST ident = child.findFirstToken(TokenTypes.IDENT); 680 if (ident != null) { 681 returnValue.add(ident); 682 } 683 child = child.getNextSibling(); 684 } 685 return returnValue; 686 } 687 688 /** 689 * Computes the exception nodes for a method. 690 * 691 * @param ast the method node. 692 * @return the list of exception nodes for ast. 693 */ 694 private static List<ExceptionInfo> getThrows(DetailAST ast) { 695 final List<ExceptionInfo> returnValue = new ArrayList<>(); 696 final DetailAST throwsAST = ast 697 .findFirstToken(TokenTypes.LITERAL_THROWS); 698 if (throwsAST != null) { 699 DetailAST child = throwsAST.getFirstChild(); 700 while (child != null) { 701 if (child.getType() == TokenTypes.IDENT 702 || child.getType() == TokenTypes.DOT) { 703 returnValue.add(getExceptionInfo(child)); 704 } 705 child = child.getNextSibling(); 706 } 707 } 708 return returnValue; 709 } 710 711 /** 712 * Get ExceptionInfo for all exceptions that throws in method code by 'throw new'. 713 * 714 * @param methodAst method DetailAST object where to find exceptions 715 * @return list of ExceptionInfo 716 */ 717 private static List<ExceptionInfo> getThrowed(DetailAST methodAst) { 718 final List<ExceptionInfo> returnValue = new ArrayList<>(); 719 final List<DetailAST> throwLiterals = findTokensInAstByType(methodAst, 720 TokenTypes.LITERAL_THROW); 721 for (DetailAST throwAst : throwLiterals) { 722 if (!isInIgnoreBlock(methodAst, throwAst)) { 723 final DetailAST newAst = throwAst.getFirstChild().getFirstChild(); 724 if (newAst.getType() == TokenTypes.LITERAL_NEW) { 725 final DetailAST child = newAst.getFirstChild(); 726 returnValue.add(getExceptionInfo(child)); 727 } 728 } 729 } 730 return returnValue; 731 } 732 733 /** 734 * Get ExceptionInfo instance. 735 * 736 * @param ast DetailAST object where to find exceptions node; 737 * @return ExceptionInfo 738 */ 739 private static ExceptionInfo getExceptionInfo(DetailAST ast) { 740 final FullIdent ident = FullIdent.createFullIdent(ast); 741 final DetailAST firstClassNameNode = getFirstClassNameNode(ast); 742 return new ExceptionInfo(firstClassNameNode, 743 new ClassInfo(new Token(ident))); 744 } 745 746 /** 747 * Get node where class name of exception starts. 748 * 749 * @param ast DetailAST object where to find exceptions node; 750 * @return exception node where class name starts 751 */ 752 private static DetailAST getFirstClassNameNode(DetailAST ast) { 753 DetailAST startNode = ast; 754 while (startNode.getType() == TokenTypes.DOT) { 755 startNode = startNode.getFirstChild(); 756 } 757 return startNode; 758 } 759 760 /** 761 * Checks if a 'throw' usage is contained within a block that should be ignored. 762 * Such blocks consist of try (with catch) blocks, local classes, anonymous classes, 763 * and lambda expressions. Note that a try block without catch is not considered. 764 * 765 * @param methodBodyAst DetailAST node representing the method body 766 * @param throwAst DetailAST node representing the 'throw' literal 767 * @return true if throwAst is inside a block that should be ignored 768 */ 769 private static boolean isInIgnoreBlock(DetailAST methodBodyAst, DetailAST throwAst) { 770 DetailAST ancestor = throwAst; 771 while (ancestor != methodBodyAst) { 772 if (ancestor.getType() == TokenTypes.LAMBDA 773 || ancestor.getType() == TokenTypes.OBJBLOCK 774 || ancestor.findFirstToken(TokenTypes.LITERAL_CATCH) != null) { 775 // throw is inside a lambda expression/anonymous class/local class, 776 // or throw is inside a try block, and there is a catch block 777 break; 778 } 779 if (ancestor.getType() == TokenTypes.LITERAL_CATCH 780 || ancestor.getType() == TokenTypes.LITERAL_FINALLY) { 781 // if the throw is inside a catch or finally block, 782 // skip the immediate ancestor (try token) 783 ancestor = ancestor.getParent(); 784 } 785 ancestor = ancestor.getParent(); 786 } 787 return ancestor != methodBodyAst; 788 } 789 790 /** 791 * Combine ExceptionInfo collections together by matching names. 792 * 793 * @param first the first collection of ExceptionInfo 794 * @param second the second collection of ExceptionInfo 795 * @return combined list of ExceptionInfo 796 */ 797 private static List<ExceptionInfo> combineExceptionInfo(Collection<ExceptionInfo> first, 798 Iterable<ExceptionInfo> second) { 799 final List<ExceptionInfo> result = new ArrayList<>(first); 800 for (ExceptionInfo exceptionInfo : second) { 801 if (result.stream().noneMatch(item -> isExceptionInfoSame(item, exceptionInfo))) { 802 result.add(exceptionInfo); 803 } 804 } 805 return result; 806 } 807 808 /** 809 * Finds node of specified type among root children, siblings, siblings children 810 * on any deep level. 811 * 812 * @param root DetailAST 813 * @param astType value of TokenType 814 * @return {@link List} of {@link DetailAST} nodes which matches the predicate. 815 */ 816 public static List<DetailAST> findTokensInAstByType(DetailAST root, int astType) { 817 final List<DetailAST> result = new ArrayList<>(); 818 // iterative preorder depth-first search 819 DetailAST curNode = root; 820 do { 821 // process curNode 822 if (curNode.getType() == astType) { 823 result.add(curNode); 824 } 825 // process children (if any) 826 if (curNode.hasChildren()) { 827 curNode = curNode.getFirstChild(); 828 continue; 829 } 830 // backtrack to parent if last child, stopping at root 831 while (curNode.getNextSibling() == null) { 832 curNode = curNode.getParent(); 833 } 834 // explore siblings if not root 835 if (curNode != root) { 836 curNode = curNode.getNextSibling(); 837 } 838 } while (curNode != root); 839 return result; 840 } 841 842 /** 843 * Checks if all record components in a compact constructor have 844 * corresponding {@code @param} tags. 845 * Reports missing or extra {@code @param} tags in the Javadoc. 846 * 847 * @param tags the list of Javadoc tags 848 * @param compactDef the compact constructor AST node 849 * @param reportExpectedTags whether to report missing {@code @param} tags 850 */ 851 private void checkRecordParamTags(final List<JavadocTag> tags, 852 final DetailAST compactDef, boolean reportExpectedTags) { 853 854 final DetailAST parent = getRecordDef(compactDef); 855 final List<DetailAST> params = getRecordComponents(parent); 856 857 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 858 while (tagIt.hasNext()) { 859 final JavadocTag tag = tagIt.next(); 860 861 if (!tag.isParamTag()) { 862 continue; 863 } 864 865 tagIt.remove(); 866 867 final String arg1 = tag.getFirstArg(); 868 final boolean found = removeMatchingParam(params, arg1); 869 870 if (!found) { 871 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, 872 JavadocTagInfo.PARAM.getText(), arg1); 873 } 874 } 875 876 if (!allowMissingParamTags && reportExpectedTags) { 877 for (DetailAST param : params) { 878 log(compactDef, MSG_EXPECTED_TAG, 879 JavadocTagInfo.PARAM.getText(), param.getText()); 880 } 881 } 882 } 883 884 /** 885 * Checks a set of tags for matching parameters. 886 * 887 * @param tags the tags to check 888 * @param parent the node which takes the parameters 889 * @param reportExpectedTags whether we should report if do not find 890 * expected tag 891 */ 892 private void checkParamTags(final List<JavadocTag> tags, 893 final DetailAST parent, boolean reportExpectedTags) { 894 final List<DetailAST> params = getParameters(parent); 895 final List<DetailAST> typeParams = CheckUtil 896 .getTypeParameters(parent); 897 898 // Loop over the tags, checking to see they exist in the params. 899 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 900 while (tagIt.hasNext()) { 901 final JavadocTag tag = tagIt.next(); 902 903 if (!tag.isParamTag()) { 904 continue; 905 } 906 907 tagIt.remove(); 908 909 final String arg1 = tag.getFirstArg(); 910 boolean found = removeMatchingParam(params, arg1); 911 912 if (arg1.endsWith(ELEMENT_END)) { 913 found = searchMatchingTypeParameter(typeParams, 914 arg1.substring(1, arg1.length() - 1)); 915 } 916 917 // Handle extra JavadocTag 918 if (!found) { 919 log(tag.getLineNo(), tag.getColumnNo(), MSG_UNUSED_TAG, 920 JavadocTagInfo.PARAM.getText(), arg1); 921 } 922 } 923 924 // Now dump out all type parameters/parameters without tags :- unless 925 // the user has chosen to suppress these problems 926 if (!allowMissingParamTags && reportExpectedTags) { 927 for (DetailAST param : params) { 928 log(param, MSG_EXPECTED_TAG, 929 JavadocTagInfo.PARAM.getText(), param.getText()); 930 } 931 932 for (DetailAST typeParam : typeParams) { 933 log(typeParam, MSG_EXPECTED_TAG, 934 JavadocTagInfo.PARAM.getText(), 935 ELEMENT_START + typeParam.findFirstToken(TokenTypes.IDENT).getText() 936 + ELEMENT_END); 937 } 938 } 939 } 940 941 /** 942 * Returns true if required type found in type parameters. 943 * 944 * @param typeParams 945 * collection of type parameters 946 * @param requiredTypeName 947 * name of required type 948 * @return true if required type found in type parameters. 949 */ 950 private static boolean searchMatchingTypeParameter(Iterable<DetailAST> typeParams, 951 String requiredTypeName) { 952 // Loop looking for matching type param 953 final Iterator<DetailAST> typeParamsIt = typeParams.iterator(); 954 boolean found = false; 955 while (typeParamsIt.hasNext()) { 956 final DetailAST typeParam = typeParamsIt.next(); 957 if (typeParam.findFirstToken(TokenTypes.IDENT).getText() 958 .equals(requiredTypeName)) { 959 found = true; 960 typeParamsIt.remove(); 961 break; 962 } 963 } 964 return found; 965 } 966 967 /** 968 * Remove parameter from params collection by name. 969 * 970 * @param params collection of DetailAST parameters 971 * @param paramName name of parameter 972 * @return true if parameter found and removed 973 */ 974 private static boolean removeMatchingParam(Iterable<DetailAST> params, String paramName) { 975 boolean found = false; 976 final Iterator<DetailAST> paramIt = params.iterator(); 977 while (paramIt.hasNext()) { 978 final DetailAST param = paramIt.next(); 979 if (param.getText().equals(paramName)) { 980 found = true; 981 paramIt.remove(); 982 break; 983 } 984 } 985 return found; 986 } 987 988 /** 989 * Checks for only one return tag. All return tags will be removed from the 990 * supplied list. 991 * 992 * @param tags the tags to check 993 * @param lineNo the line number of the expected tag 994 * @param reportExpectedTags whether we should report if do not find 995 * expected tag 996 */ 997 private void checkReturnTag(List<JavadocTag> tags, int lineNo, 998 boolean reportExpectedTags) { 999 // Loop over tags finding return tags. After the first one, report a violation 1000 boolean found = false; 1001 final ListIterator<JavadocTag> it = tags.listIterator(); 1002 while (it.hasNext()) { 1003 final JavadocTag javadocTag = it.next(); 1004 if (javadocTag.isReturnTag()) { 1005 if (found) { 1006 log(javadocTag.getLineNo(), javadocTag.getColumnNo(), 1007 MSG_DUPLICATE_TAG, 1008 JavadocTagInfo.RETURN.getText()); 1009 } 1010 found = true; 1011 it.remove(); 1012 } 1013 } 1014 1015 // Handle there being no @return tags :- unless 1016 // the user has chosen to suppress these problems 1017 if (!found && !allowMissingReturnTag && reportExpectedTags) { 1018 log(lineNo, MSG_RETURN_EXPECTED); 1019 } 1020 } 1021 1022 /** 1023 * Checks a set of tags for matching throws. 1024 * 1025 * @param tags the tags to check 1026 * @param throwsList the throws to check 1027 * @param reportExpectedTags whether we should report if do not find 1028 * expected tag 1029 */ 1030 private void checkThrowsTags(List<JavadocTag> tags, 1031 List<ExceptionInfo> throwsList, boolean reportExpectedTags) { 1032 // Loop over the tags, checking to see they exist in the throws. 1033 final ListIterator<JavadocTag> tagIt = tags.listIterator(); 1034 while (tagIt.hasNext()) { 1035 final JavadocTag tag = tagIt.next(); 1036 1037 if (!tag.isThrowsTag()) { 1038 continue; 1039 } 1040 tagIt.remove(); 1041 1042 // Loop looking for matching throw 1043 processThrows(throwsList, tag.getFirstArg()); 1044 } 1045 // Now dump out all throws without tags :- unless 1046 // the user has chosen to suppress these problems 1047 if (validateThrows && reportExpectedTags) { 1048 throwsList.stream().filter(exceptionInfo -> !exceptionInfo.isFound()) 1049 .forEach(exceptionInfo -> { 1050 final Token token = exceptionInfo.getName(); 1051 log(exceptionInfo.getAst(), 1052 MSG_EXPECTED_TAG, 1053 JavadocTagInfo.THROWS.getText(), token.getText()); 1054 }); 1055 } 1056 } 1057 1058 /** 1059 * Verifies that documented exception is in throws. 1060 * 1061 * @param throwsIterable collection of throws 1062 * @param documentedClassName documented exception class name 1063 */ 1064 private static void processThrows(Iterable<ExceptionInfo> throwsIterable, 1065 String documentedClassName) { 1066 for (ExceptionInfo exceptionInfo : throwsIterable) { 1067 if (isClassNamesSame(exceptionInfo.getName().getText(), 1068 documentedClassName)) { 1069 exceptionInfo.setFound(); 1070 break; 1071 } 1072 } 1073 } 1074 1075 /** 1076 * Check that ExceptionInfo objects are same by name. 1077 * 1078 * @param info1 ExceptionInfo object 1079 * @param info2 ExceptionInfo object 1080 * @return true is ExceptionInfo object have the same name 1081 */ 1082 private static boolean isExceptionInfoSame(ExceptionInfo info1, ExceptionInfo info2) { 1083 return isClassNamesSame(info1.getName().getText(), 1084 info2.getName().getText()); 1085 } 1086 1087 /** 1088 * Check that class names are same by short name of class. If some class name is fully 1089 * qualified it is cut to short name. 1090 * 1091 * @param class1 class name 1092 * @param class2 class name 1093 * @return true is ExceptionInfo object have the same name 1094 */ 1095 private static boolean isClassNamesSame(String class1, String class2) { 1096 final String class1ShortName = class1 1097 .substring(class1.lastIndexOf('.') + 1); 1098 final String class2ShortName = class2 1099 .substring(class2.lastIndexOf('.') + 1); 1100 return class1ShortName.equals(class2ShortName); 1101 } 1102 1103 /** 1104 * Contains class's {@code Token}. 1105 */ 1106 private static class ClassInfo { 1107 1108 /** {@code FullIdent} associated with this class. */ 1109 private final Token name; 1110 1111 /** 1112 * Creates new instance of class information object. 1113 * 1114 * @param className token which represents class name. 1115 * @throws IllegalArgumentException when className is nulls 1116 */ 1117 /* package */ ClassInfo(final Token className) { 1118 name = className; 1119 } 1120 1121 /** 1122 * Gets class name. 1123 * 1124 * @return class name 1125 */ 1126 /* package */ final Token getName() { 1127 return name; 1128 } 1129 1130 } 1131 1132 /** 1133 * Represents text element with location in the text. 1134 */ 1135 private static final class Token { 1136 1137 /** Token's text. */ 1138 private final String text; 1139 1140 /** 1141 * Converts FullIdent to Token. 1142 * 1143 * @param fullIdent full ident to convert. 1144 */ 1145 private Token(FullIdent fullIdent) { 1146 text = fullIdent.getText(); 1147 } 1148 1149 /** 1150 * Gets text of the token. 1151 * 1152 * @return text of the token 1153 */ 1154 /* package */ String getText() { 1155 return text; 1156 } 1157 1158 } 1159 1160 /** Stores useful information about declared exception. */ 1161 private static final class ExceptionInfo { 1162 1163 /** AST node representing this exception. */ 1164 private final DetailAST ast; 1165 1166 /** Class information associated with this exception. */ 1167 private final ClassInfo classInfo; 1168 /** Does the exception have throws tag associated with. */ 1169 private boolean found; 1170 1171 /** 1172 * Creates new instance for {@code FullIdent}. 1173 * 1174 * @param ast AST node representing this exception 1175 * @param classInfo class info 1176 */ 1177 private ExceptionInfo(DetailAST ast, ClassInfo classInfo) { 1178 this.ast = ast; 1179 this.classInfo = classInfo; 1180 } 1181 1182 /** 1183 * Gets the AST node representing this exception. 1184 * 1185 * @return the AST node representing this exception 1186 */ 1187 private DetailAST getAst() { 1188 return ast; 1189 } 1190 1191 /** Mark that the exception has associated throws tag. */ 1192 private void setFound() { 1193 found = true; 1194 } 1195 1196 /** 1197 * Checks that the exception has throws tag associated with it. 1198 * 1199 * @return whether the exception has throws tag associated with 1200 */ 1201 private boolean isFound() { 1202 return found; 1203 } 1204 1205 /** 1206 * Gets exception name. 1207 * 1208 * @return exception's name 1209 */ 1210 private Token getName() { 1211 return classInfo.getName(); 1212 } 1213 1214 } 1215 1216}