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