001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2021 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.Arrays; 023import java.util.Collections; 024import java.util.HashSet; 025import java.util.Set; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import com.puppycrawl.tools.checkstyle.StatelessCheck; 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.DetailNode; 032import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 033import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 034import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 035 036/** 037 * <p> 038 * Checks that 039 * <a href="https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html#firstsentence"> 040 * Javadoc summary sentence</a> does not contain phrases that are not recommended to use. 041 * Summaries that contain only the {@code {@inheritDoc}} tag are skipped. 042 * Check also violate Javadoc that does not contain first sentence. 043 * </p> 044 * <ul> 045 * <li> 046 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations 047 * if the Javadoc being examined by this check violates the tight html rules defined at 048 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">Tight-HTML Rules</a>. 049 * Type is {@code boolean}. 050 * Default value is {@code false}. 051 * </li> 052 * <li> 053 * Property {@code forbiddenSummaryFragments} - Specify the regexp for forbidden summary fragments. 054 * Type is {@code java.util.regex.Pattern}. 055 * Default value is {@code "^$"}. 056 * </li> 057 * <li> 058 * Property {@code period} - Specify the period symbol at the end of first javadoc sentence. 059 * Type is {@code java.lang.String}. 060 * Default value is {@code "."}. 061 * </li> 062 * </ul> 063 * <p> 064 * To configure the default check to validate that first sentence is not empty and first 065 * sentence is not missing: 066 * </p> 067 * <pre> 068 * <module name="SummaryJavadocCheck"/> 069 * </pre> 070 * <p> 071 * Example of {@code {@inheritDoc}} without summary. 072 * </p> 073 * <pre> 074 * public class Test extends Exception { 075 * //Valid 076 * /** 077 * * {@inheritDoc} 078 * */ 079 * public String ValidFunction(){ 080 * return ""; 081 * } 082 * //Violation 083 * /** 084 * * 085 * */ 086 * public String InvalidFunction(){ 087 * return ""; 088 * } 089 * } 090 * </pre> 091 * <p> 092 * Example of non permitted empty javadoc for Inline Summary Javadoc. 093 * </p> 094 * <pre> 095 * public class Test extends Exception { 096 * /** 097 * * {@summary } 098 * */ 099 * public String InvalidFunctionOne(){ // violation 100 * return ""; 101 * } 102 * 103 * /** 104 * * {@summary <p> <p/>} 105 * */ 106 * public String InvalidFunctionTwo(){ // violation 107 * return ""; 108 * } 109 * 110 * /** 111 * * {@summary <p>This is summary for validFunctionThree.<p/>} 112 * */ 113 * public void validFunctionThree(){} // ok 114 * } 115 * </pre> 116 * <p> 117 * To ensure that summary do not contain phrase like "This method returns", 118 * use following config: 119 * </p> 120 * <pre> 121 * <module name="SummaryJavadocCheck"> 122 * <property name="forbiddenSummaryFragments" 123 * value="^This method returns.*"/> 124 * </module> 125 * </pre> 126 * <p> 127 * To specify period symbol at the end of first javadoc sentence: 128 * </p> 129 * <pre> 130 * <module name="SummaryJavadocCheck"> 131 * <property name="period" value="。"/> 132 * </module> 133 * </pre> 134 * <p> 135 * Example of period property. 136 * </p> 137 * <pre> 138 * public class TestClass { 139 * /** 140 * * This is invalid java doc. 141 * */ 142 * void invalidJavaDocMethod() { 143 * } 144 * /** 145 * * This is valid java doc。 146 * */ 147 * void validJavaDocMethod() { 148 * } 149 * } 150 * </pre> 151 * <p> 152 * Example of period property for inline summary javadoc. 153 * </p> 154 * <pre> 155 * public class TestClass { 156 * /** 157 * * {@summary This is invalid java doc.} 158 * */ 159 * public void invalidJavaDocMethod() { // violation 160 * } 161 * /** 162 * * {@summary This is valid java doc。} 163 * */ 164 * public void validJavaDocMethod() { // ok 165 * } 166 * } 167 * </pre> 168 * <p> 169 * Example of inline summary javadoc with HTML tags. 170 * </p> 171 * <pre> 172 * public class Test { 173 * /** 174 * * {@summary First sentence is normally the summary. 175 * * Use of html tags: 176 * * <ul> 177 * * <li>Item one.</li> 178 * * <li>Item two.</li> 179 * * </ul>} 180 * */ 181 * public void validInlineJavadoc() { // ok 182 * } 183 * } 184 * </pre> 185 * <p> 186 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 187 * </p> 188 * <p> 189 * Violation Message Keys: 190 * </p> 191 * <ul> 192 * <li> 193 * {@code javadoc.missed.html.close} 194 * </li> 195 * <li> 196 * {@code javadoc.parse.rule.error} 197 * </li> 198 * <li> 199 * {@code javadoc.wrong.singleton.html.tag} 200 * </li> 201 * <li> 202 * {@code summary.first.sentence} 203 * </li> 204 * <li> 205 * {@code summary.javaDoc} 206 * </li> 207 * <li> 208 * {@code summary.javaDoc.missing} 209 * </li> 210 * <li> 211 * {@code summary.javaDoc.missing.period} 212 * </li> 213 * </ul> 214 * 215 * @since 6.0 216 */ 217@StatelessCheck 218public class SummaryJavadocCheck extends AbstractJavadocCheck { 219 220 /** 221 * A key is pointing to the warning message text in "messages.properties" 222 * file. 223 */ 224 public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence"; 225 226 /** 227 * A key is pointing to the warning message text in "messages.properties" 228 * file. 229 */ 230 public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc"; 231 232 /** 233 * A key is pointing to the warning message text in "messages.properties" 234 * file. 235 */ 236 public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing"; 237 238 /** 239 * A key is pointing to the warning message text in "messages.properties" file. 240 */ 241 public static final String MSG_SUMMARY_MISSING_PERIOD = "summary.javaDoc.missing.period"; 242 243 /** 244 * This regexp is used to convert multiline javadoc to single line without stars. 245 */ 246 private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN = 247 Pattern.compile("\n[ ]+(\\*)|^[ ]+(\\*)"); 248 249 /** 250 * This regexp is used to remove html tags, whitespace, and asterisks from a string. 251 */ 252 private static final Pattern HTML_ELEMENTS = 253 Pattern.compile("<[^>]*>"); 254 255 /** 256 * This regexp is used to extract the content of a summary javadoc tag. 257 */ 258 private static final Pattern SUMMARY_PATTERN = Pattern.compile("\\{@summary ([\\S\\s]+)}"); 259 /** Period literal. */ 260 private static final String PERIOD = "."; 261 262 /** Summary tag text. */ 263 private static final String SUMMARY_TEXT = "@summary"; 264 265 /** Set of allowed Tokens tags in summary java doc. */ 266 private static final Set<Integer> ALLOWED_TYPES = Collections.unmodifiableSet( 267 new HashSet<>(Arrays.asList( 268 JavadocTokenTypes.WS, 269 JavadocTokenTypes.DESCRIPTION, 270 JavadocTokenTypes.TEXT)) 271 ); 272 273 /** 274 * Specify the regexp for forbidden summary fragments. 275 */ 276 private Pattern forbiddenSummaryFragments = CommonUtil.createPattern("^$"); 277 278 /** 279 * Specify the period symbol at the end of first javadoc sentence. 280 */ 281 private String period = PERIOD; 282 283 /** 284 * Setter to specify the regexp for forbidden summary fragments. 285 * 286 * @param pattern a pattern. 287 */ 288 public void setForbiddenSummaryFragments(Pattern pattern) { 289 forbiddenSummaryFragments = pattern; 290 } 291 292 /** 293 * Setter to specify the period symbol at the end of first javadoc sentence. 294 * 295 * @param period period's value. 296 */ 297 public void setPeriod(String period) { 298 this.period = period; 299 } 300 301 @Override 302 public int[] getDefaultJavadocTokens() { 303 return new int[] { 304 JavadocTokenTypes.JAVADOC, 305 }; 306 } 307 308 @Override 309 public int[] getRequiredJavadocTokens() { 310 return getAcceptableJavadocTokens(); 311 } 312 313 @Override 314 public void visitJavadocToken(DetailNode ast) { 315 if (containsSummaryTag(ast)) { 316 validateSummaryTag(ast); 317 } 318 else if (!startsWithInheritDoc(ast)) { 319 final String summaryDoc = getSummarySentence(ast); 320 if (summaryDoc.isEmpty()) { 321 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING); 322 } 323 else if (!period.isEmpty()) { 324 final String firstSentence = getFirstSentence(ast); 325 final int endOfSentence = firstSentence.lastIndexOf(period); 326 if (!summaryDoc.contains(period)) { 327 log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE); 328 } 329 if (endOfSentence != -1 330 && containsForbiddenFragment(firstSentence.substring(0, endOfSentence))) { 331 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC); 332 } 333 } 334 } 335 } 336 337 /** 338 * Checks if summary tag present. 339 * 340 * @param javadoc javadoc root node. 341 * @return {@code true} if first sentence contains @summary tag. 342 */ 343 private static boolean containsSummaryTag(DetailNode javadoc) { 344 final DetailNode node = getFirstInlineTag(javadoc); 345 return node != null && isSummaryTag(node); 346 } 347 348 /** 349 * Finds and returns the first inline tag node from a javadoc root node. 350 * 351 * @param javadoc javadoc root node. 352 * @return first inline tag node or null if no node is found. 353 */ 354 private static DetailNode getFirstInlineTag(DetailNode javadoc) { 355 DetailNode node = null; 356 final DetailNode[] children = javadoc.getChildren(); 357 for (DetailNode child: children) { 358 // If present as a children of javadoc 359 if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) { 360 node = child; 361 } 362 // If nested inside html tag 363 else if (child.getType() == JavadocTokenTypes.HTML_ELEMENT) { 364 node = getInlineTagNodeWithinHtmlElement(child); 365 } 366 367 if (node != null) { 368 break; 369 } 370 } 371 return node; 372 } 373 374 /** 375 * Returns an inline javadoc tag node that is within a html tag. 376 * 377 * @param ast html tag node. 378 * @return inline summary javadoc tag node or null if no node is found. 379 */ 380 private static DetailNode getInlineTagNodeWithinHtmlElement(DetailNode ast) { 381 DetailNode node = ast; 382 DetailNode result = null; 383 // node can never be null as this method is called when there is a HTML_ELEMENT 384 if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) { 385 result = node; 386 } 387 else if (node.getType() == JavadocTokenTypes.HTML_TAG) { 388 // HTML_TAG always has more than 2 children. 389 node = node.getChildren()[1]; 390 result = getInlineTagNodeWithinHtmlElement(node); 391 } 392 else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT 393 // Condition for SINGLETON html element which cannot contain summary node 394 && node.getChildren()[0].getChildren().length > 1) { 395 // Html elements have one tested tag before actual content inside it 396 node = node.getChildren()[0].getChildren()[1]; 397 result = getInlineTagNodeWithinHtmlElement(node); 398 } 399 return result; 400 } 401 402 /** 403 * Checks if the first tag inside ast is summary tag. 404 * 405 * @param javadoc root node. 406 * @return {@code true} if first tag is summary tag. 407 */ 408 private static boolean isSummaryTag(DetailNode javadoc) { 409 final DetailNode[] child = javadoc.getChildren(); 410 411 // Checking size of ast is not required, since ast contains 412 // children of Inline Tag, as at least 2 children will be present which are 413 // RCURLY and LCURLY. 414 return child[1].getType() == JavadocTokenTypes.CUSTOM_NAME 415 && SUMMARY_TEXT.equals(child[1].getText()); 416 } 417 418 /** 419 * Checks the inline summary (if present) for {@code period} at end and forbidden fragments. 420 * 421 * @param ast javadoc root node. 422 */ 423 private void validateSummaryTag(DetailNode ast) { 424 final String inlineSummary = getInlineSummary(); 425 final String summaryVisible = getVisibleContent(inlineSummary); 426 if (summaryVisible.isEmpty()) { 427 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING); 428 } 429 else if (!period.isEmpty()) { 430 if (isPeriodAtEnd(summaryVisible, period)) { 431 log(ast.getLineNumber(), MSG_SUMMARY_MISSING_PERIOD); 432 } 433 else if (containsForbiddenFragment(inlineSummary)) { 434 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC); 435 } 436 } 437 } 438 439 /** 440 * Gets entire content of summary tag. 441 * 442 * @return summary sentence of javadoc root node. 443 */ 444 private String getInlineSummary() { 445 final DetailAST blockCommentAst = getBlockCommentAst(); 446 final String javadocText = blockCommentAst.getFirstChild().getText(); 447 final Matcher matcher = SUMMARY_PATTERN.matcher(javadocText); 448 String comment = ""; 449 if (matcher.find()) { 450 comment = matcher.group(1); 451 } 452 comment = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN.matcher(comment) 453 .replaceAll(""); 454 return comment; 455 } 456 457 /** 458 * Gets the string that is visible to user in javadoc. 459 * 460 * @param summary entire content of summary javadoc. 461 * @return string that is visible to user in javadoc. 462 */ 463 private static String getVisibleContent(String summary) { 464 final String visibleSummary = HTML_ELEMENTS.matcher(summary).replaceAll(""); 465 return visibleSummary.trim(); 466 } 467 468 /** 469 * Checks if the string ends with period. 470 * 471 * @param sentence string to check for period at end. 472 * @param period string to check within sentence. 473 * @return {@code true} if sentence ends with period. 474 */ 475 private static boolean isPeriodAtEnd(String sentence, String period) { 476 final String summarySentence = sentence.trim(); 477 return summarySentence.lastIndexOf(period) != summarySentence.length() - 1; 478 } 479 480 /** 481 * Tests if first sentence contains forbidden summary fragment. 482 * 483 * @param firstSentence string with first sentence. 484 * @return {@code true} if first sentence contains forbidden summary fragment. 485 */ 486 private boolean containsForbiddenFragment(String firstSentence) { 487 final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN 488 .matcher(firstSentence).replaceAll(" ").trim(); 489 return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find(); 490 } 491 492 /** 493 * Trims the given {@code text} of duplicate whitespaces. 494 * 495 * @param text the text to transform. 496 * @return the finalized form of the text. 497 */ 498 private static String trimExcessWhitespaces(String text) { 499 final StringBuilder result = new StringBuilder(256); 500 boolean previousWhitespace = true; 501 502 for (char letter : text.toCharArray()) { 503 final char print; 504 if (Character.isWhitespace(letter)) { 505 if (previousWhitespace) { 506 continue; 507 } 508 509 previousWhitespace = true; 510 print = ' '; 511 } 512 else { 513 previousWhitespace = false; 514 print = letter; 515 } 516 517 result.append(print); 518 } 519 520 return result.toString(); 521 } 522 523 /** 524 * Checks if the node starts with an {@inheritDoc}. 525 * 526 * @param root the root node to examine. 527 * @return {@code true} if the javadoc starts with an {@inheritDoc}. 528 */ 529 private static boolean startsWithInheritDoc(DetailNode root) { 530 boolean found = false; 531 final DetailNode[] children = root.getChildren(); 532 533 for (int i = 0; !found; i++) { 534 final DetailNode child = children[i]; 535 if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG 536 && child.getChildren()[1].getType() == JavadocTokenTypes.INHERIT_DOC_LITERAL) { 537 found = true; 538 } 539 else if (child.getType() != JavadocTokenTypes.LEADING_ASTERISK 540 && !CommonUtil.isBlank(child.getText())) { 541 break; 542 } 543 } 544 545 return found; 546 } 547 548 /** 549 * Finds and returns summary sentence. 550 * 551 * @param ast javadoc root node. 552 * @return violation string. 553 */ 554 private static String getSummarySentence(DetailNode ast) { 555 boolean flag = true; 556 final StringBuilder result = new StringBuilder(256); 557 for (DetailNode child : ast.getChildren()) { 558 if (ALLOWED_TYPES.contains(child.getType())) { 559 result.append(child.getText()); 560 } 561 else if (child.getType() == JavadocTokenTypes.HTML_ELEMENT 562 && CommonUtil.isBlank(result.toString().trim())) { 563 result.append(getStringInsideTag(result.toString(), 564 child.getChildren()[0].getChildren()[0])); 565 } 566 else if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) { 567 flag = false; 568 } 569 if (!flag) { 570 break; 571 } 572 } 573 return result.toString().trim(); 574 } 575 576 /** 577 * Get concatenated string within text of html tags. 578 * 579 * @param result javadoc string 580 * @param detailNode javadoc tag node 581 * @return java doc tag content appended in result 582 */ 583 private static String getStringInsideTag(String result, DetailNode detailNode) { 584 final StringBuilder contents = new StringBuilder(result); 585 DetailNode tempNode = detailNode; 586 while (tempNode != null) { 587 if (tempNode.getType() == JavadocTokenTypes.TEXT) { 588 contents.append(tempNode.getText()); 589 } 590 tempNode = JavadocUtil.getNextSibling(tempNode); 591 } 592 return contents.toString(); 593 } 594 595 /** 596 * Finds and returns first sentence. 597 * 598 * @param ast Javadoc root node. 599 * @return first sentence. 600 */ 601 private static String getFirstSentence(DetailNode ast) { 602 final StringBuilder result = new StringBuilder(256); 603 final String periodSuffix = PERIOD + ' '; 604 for (DetailNode child : ast.getChildren()) { 605 final String text; 606 if (child.getChildren().length == 0) { 607 text = child.getText(); 608 } 609 else { 610 text = getFirstSentence(child); 611 } 612 613 if (text.contains(periodSuffix)) { 614 result.append(text, 0, text.indexOf(periodSuffix) + 1); 615 break; 616 } 617 618 result.append(text); 619 } 620 return result.toString(); 621 } 622 623}