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