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