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.Set; 023 024import javax.annotation.Nullable; 025 026import com.puppycrawl.tools.checkstyle.StatelessCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailNode; 028import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 030import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 031 032/** 033 * <div> 034 * Checks the Javadoc paragraph. 035 * </div> 036 * 037 * <p> 038 * Checks that: 039 * </p> 040 * <ul> 041 * <li>There is one blank line between each of two paragraphs.</li> 042 * <li>Each paragraph but the first has {@literal <p>} immediately 043 * before the first word, with no space after.</li> 044 * <li>The outer most paragraph tags should not precede 045 * <a href="https://www.w3schools.com/html/html_blocks.asp">HTML block-tag</a>. 046 * Nested paragraph tags are allowed to do that. This check only supports following block-tags: 047 * {@literal <address>},{@literal <blockquote>} 048 * ,{@literal <div>},{@literal <dl>} 049 * ,{@literal <h1>},{@literal <h2>},{@literal <h3>},{@literal <h4>},{@literal <h5>}, 050 * {@literal <h6>},{@literal <hr>} 051 * ,{@literal <ol>},{@literal <p>},{@literal <pre>} 052 * ,{@literal <table>},{@literal <ul>}. 053 * </li> 054 * </ul> 055 * 056 * <p><b>ATTENTION:</b></p> 057 * 058 * <p>This Check ignores HTML comments.</p> 059 * 060 * <p>The Check ignores all the nested paragraph tags, 061 * it will not give any kind of violation if the paragraph tag is nested. 062 * It also ignores paragraph tags inside block tags.</p> 063 * 064 * @since 6.0 065 */ 066@StatelessCheck 067public class JavadocParagraphCheck extends AbstractJavadocCheck { 068 069 /** 070 * A key is pointing to the warning message text in "messages.properties" 071 * file. 072 */ 073 public static final String MSG_TAG_AFTER = "javadoc.paragraph.tag.after"; 074 075 /** 076 * A key is pointing to the warning message text in "messages.properties" 077 * file. 078 */ 079 public static final String MSG_LINE_BEFORE = "javadoc.paragraph.line.before"; 080 081 /** 082 * A key is pointing to the warning message text in "messages.properties" 083 * file. 084 */ 085 public static final String MSG_REDUNDANT_PARAGRAPH = "javadoc.paragraph.redundant.paragraph"; 086 087 /** 088 * A key is pointing to the warning message text in "messages.properties" 089 * file. 090 */ 091 public static final String MSG_MISPLACED_TAG = "javadoc.paragraph.misplaced.tag"; 092 093 /** 094 * A key is pointing to the warning message text in "messages.properties" 095 * file. 096 */ 097 public static final String MSG_PRECEDED_BLOCK_TAG = "javadoc.paragraph.preceded.block.tag"; 098 099 /** 100 * Constant for the paragraph tag name. 101 */ 102 private static final String PARAGRAPH_TAG = "p"; 103 104 /** 105 * Set of block tags supported by this check. 106 */ 107 private static final Set<String> BLOCK_TAGS = 108 Set.of("address", "blockquote", "div", "dl", 109 "h1", "h2", "h3", "h4", "h5", "h6", "hr", 110 "ol", PARAGRAPH_TAG, "pre", "table", "ul"); 111 112 /** 113 * Control whether the {@literal <p>} tag should be placed immediately before the first word. 114 */ 115 private boolean allowNewlineParagraph = true; 116 117 /** 118 * Creates a new {@code JavadocParagraphCheck} instance. 119 */ 120 public JavadocParagraphCheck() { 121 // no code by default 122 } 123 124 /** 125 * Setter to control whether the {@literal <p>} tag should be placed 126 * immediately before the first word. 127 * 128 * @param value value to set. 129 * @since 6.9 130 */ 131 public void setAllowNewlineParagraph(boolean value) { 132 allowNewlineParagraph = value; 133 } 134 135 @Override 136 public int[] getDefaultJavadocTokens() { 137 return new int[] { 138 JavadocCommentsTokenTypes.NEWLINE, 139 JavadocCommentsTokenTypes.HTML_ELEMENT, 140 }; 141 } 142 143 @Override 144 public int[] getRequiredJavadocTokens() { 145 return getAcceptableJavadocTokens(); 146 } 147 148 @Override 149 public void visitJavadocToken(DetailNode ast) { 150 if (ast.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(ast)) { 151 checkEmptyLine(ast); 152 } 153 else if (JavadocUtil.isTag(ast, PARAGRAPH_TAG)) { 154 checkParagraphTag(ast); 155 } 156 } 157 158 /** 159 * Determines whether or not the next line after empty line has paragraph tag in the beginning. 160 * 161 * @param newline NEWLINE node. 162 */ 163 private void checkEmptyLine(DetailNode newline) { 164 final DetailNode nearestToken = getNearestNode(newline); 165 if (nearestToken != null && nearestToken.getType() == JavadocCommentsTokenTypes.TEXT 166 && !CommonUtil.isBlank(nearestToken.getText())) { 167 log(newline, MSG_TAG_AFTER); 168 } 169 } 170 171 /** 172 * Determines whether or not the line with paragraph tag has previous empty line. 173 * 174 * @param tag html tag. 175 */ 176 private void checkParagraphTag(DetailNode tag) { 177 if (!isNestedParagraph(tag) && !isInsideBlockTag(tag)) { 178 final DetailNode newLine = getNearestEmptyLine(tag); 179 if (isFirstParagraph(tag)) { 180 log(tag, MSG_REDUNDANT_PARAGRAPH); 181 } 182 else if (newLine == null || tag.getLineNumber() - newLine.getLineNumber() != 1) { 183 log(tag, MSG_LINE_BEFORE); 184 } 185 186 final String blockTagName = findFollowedBlockTagName(tag); 187 if (blockTagName != null) { 188 log(tag, MSG_PRECEDED_BLOCK_TAG, blockTagName); 189 } 190 191 if (!allowNewlineParagraph && isImmediatelyFollowedByNewLine(tag)) { 192 log(tag, MSG_MISPLACED_TAG); 193 } 194 if (isImmediatelyFollowedByText(tag)) { 195 log(tag, MSG_MISPLACED_TAG); 196 } 197 } 198 } 199 200 /** 201 * Determines whether the paragraph tag is nested. 202 * 203 * @param tag html tag. 204 * @return true, if the paragraph tag is nested. 205 */ 206 private static boolean isNestedParagraph(DetailNode tag) { 207 boolean nested = false; 208 DetailNode parent = tag.getParent(); 209 210 while (parent != null) { 211 if (parent.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) { 212 nested = true; 213 break; 214 } 215 parent = parent.getParent(); 216 } 217 218 return nested; 219 } 220 221 /** 222 * Determines whether the paragraph tag is inside javadoc block tag. 223 * 224 * @param tag html tag. 225 * @return true, if the paragraph tag is inside javadoc block tag. 226 */ 227 private static boolean isInsideBlockTag(DetailNode tag) { 228 boolean result = false; 229 DetailNode parent = tag; 230 231 while (parent != null) { 232 if (parent.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) { 233 result = true; 234 break; 235 } 236 parent = parent.getParent(); 237 } 238 239 return result; 240 } 241 242 /** 243 * Determines whether or not the paragraph tag is followed by block tag. 244 * 245 * @param tag html tag. 246 * @return block tag if the paragraph tag is followed by block tag or null if not found. 247 */ 248 @Nullable 249 private static String findFollowedBlockTagName(DetailNode tag) { 250 final DetailNode htmlElement = findFirstHtmlElementAfter(tag); 251 String blockTagName = null; 252 253 if (htmlElement != null) { 254 blockTagName = getHtmlElementName(htmlElement); 255 } 256 257 return blockTagName; 258 } 259 260 /** 261 * Finds and returns first html element after the tag. 262 * 263 * @param tag html tag. 264 * @return first html element after the paragraph tag or null if not found. 265 */ 266 @Nullable 267 private static DetailNode findFirstHtmlElementAfter(DetailNode tag) { 268 DetailNode htmlElement = getNextSibling(tag); 269 270 while (htmlElement != null 271 && htmlElement.getType() != JavadocCommentsTokenTypes.HTML_ELEMENT) { 272 if (htmlElement.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 273 htmlElement = htmlElement.getFirstChild(); 274 } 275 else if (htmlElement.getType() == JavadocCommentsTokenTypes.TEXT 276 && !CommonUtil.isBlank(htmlElement.getText())) { 277 htmlElement = null; 278 break; 279 } 280 else { 281 htmlElement = htmlElement.getNextSibling(); 282 } 283 } 284 if (htmlElement != null 285 && JavadocUtil.findFirstToken(htmlElement, 286 JavadocCommentsTokenTypes.HTML_TAG_END) == null) { 287 htmlElement = null; 288 } 289 290 return htmlElement; 291 } 292 293 /** 294 * Finds and returns first block-level html element name. 295 * 296 * @param htmlElement block-level html tag. 297 * @return block-level html element name or null if not found. 298 */ 299 @Nullable 300 private static String getHtmlElementName(DetailNode htmlElement) { 301 final DetailNode htmlTagStart = htmlElement.getFirstChild(); 302 final DetailNode htmlTagName = 303 JavadocUtil.findFirstToken(htmlTagStart, JavadocCommentsTokenTypes.TAG_NAME); 304 String blockTagName = null; 305 if (BLOCK_TAGS.contains(htmlTagName.getText())) { 306 blockTagName = htmlTagName.getText(); 307 } 308 309 return blockTagName; 310 } 311 312 /** 313 * Returns nearest node. 314 * 315 * @param node DetailNode node. 316 * @return nearest node. 317 */ 318 private static DetailNode getNearestNode(DetailNode node) { 319 DetailNode currentNode = node; 320 while (currentNode != null 321 && (currentNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK 322 || currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE)) { 323 currentNode = currentNode.getNextSibling(); 324 } 325 if (currentNode != null 326 && currentNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 327 currentNode = currentNode.getFirstChild(); 328 } 329 return currentNode; 330 } 331 332 /** 333 * Determines whether or not the line is empty line. 334 * 335 * @param newLine NEWLINE node. 336 * @return true, if line is empty line. 337 */ 338 private static boolean isEmptyLine(DetailNode newLine) { 339 boolean result = false; 340 DetailNode previousSibling = newLine.getPreviousSibling(); 341 if (previousSibling != null && (previousSibling.getParent().getType() 342 == JavadocCommentsTokenTypes.JAVADOC_CONTENT 343 || insideNonTightHtml(previousSibling))) { 344 if (previousSibling.getType() == JavadocCommentsTokenTypes.TEXT 345 && CommonUtil.isBlank(previousSibling.getText())) { 346 previousSibling = previousSibling.getPreviousSibling(); 347 } 348 result = previousSibling != null 349 && previousSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK; 350 } 351 return result; 352 } 353 354 /** 355 * Checks whether the given node is inside a non-tight HTML element. 356 * 357 * @param previousSibling the node to check 358 * @return true if inside non-tight HTML, false otherwise 359 */ 360 private static boolean insideNonTightHtml(DetailNode previousSibling) { 361 final DetailNode parent = previousSibling.getParent(); 362 DetailNode htmlElement = parent; 363 if (parent.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 364 htmlElement = parent.getParent(); 365 } 366 return htmlElement.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT 367 && JavadocUtil.findFirstToken(htmlElement, 368 JavadocCommentsTokenTypes.HTML_TAG_END) == null; 369 } 370 371 /** 372 * Determines whether or not the line with paragraph tag is first line in javadoc. 373 * 374 * @param paragraphTag paragraph tag. 375 * @return true, if line with paragraph tag is first line in javadoc. 376 */ 377 private static boolean isFirstParagraph(DetailNode paragraphTag) { 378 boolean result = true; 379 DetailNode previousNode = paragraphTag.getPreviousSibling(); 380 while (previousNode != null) { 381 if (previousNode.getType() == JavadocCommentsTokenTypes.TEXT 382 && !CommonUtil.isBlank(previousNode.getText()) 383 || previousNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK 384 && previousNode.getType() != JavadocCommentsTokenTypes.NEWLINE 385 && previousNode.getType() != JavadocCommentsTokenTypes.TEXT) { 386 result = false; 387 break; 388 } 389 previousNode = previousNode.getPreviousSibling(); 390 } 391 return result; 392 } 393 394 /** 395 * Finds and returns nearest empty line in javadoc. 396 * 397 * @param node DetailNode node. 398 * @return Some nearest empty line in javadoc. 399 */ 400 private static DetailNode getNearestEmptyLine(DetailNode node) { 401 DetailNode newLine = node; 402 while (newLine != null) { 403 final DetailNode previousSibling = newLine.getPreviousSibling(); 404 if (newLine.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(newLine)) { 405 break; 406 } 407 newLine = previousSibling; 408 } 409 return newLine; 410 } 411 412 /** 413 * Tests whether the paragraph tag is immediately followed by the text. 414 * 415 * @param tag html tag. 416 * @return true, if the paragraph tag is immediately followed by the text. 417 */ 418 private static boolean isImmediatelyFollowedByText(DetailNode tag) { 419 final DetailNode nextSibling = getNextSibling(tag); 420 421 return nextSibling == null || nextSibling.getText().startsWith(" "); 422 } 423 424 /** 425 * Tests whether the paragraph tag is immediately followed by the new line. 426 * 427 * @param tag html tag. 428 * @return true, if the paragraph tag is immediately followed by the new line. 429 */ 430 private static boolean isImmediatelyFollowedByNewLine(DetailNode tag) { 431 final DetailNode sibling = getNextSibling(tag); 432 return sibling != null && sibling.getType() == JavadocCommentsTokenTypes.NEWLINE; 433 } 434 435 /** 436 * Custom getNextSibling method to handle different types of paragraph tag. 437 * It works for both {@code <p>} and {@code <p></p>} tags. 438 * 439 * @param tag HTML_ELEMENT tag. 440 * @return next sibling of the tag. 441 */ 442 private static DetailNode getNextSibling(DetailNode tag) { 443 DetailNode nextSibling; 444 final DetailNode paragraphStartTagToken = tag.getFirstChild(); 445 final DetailNode nextNode = paragraphStartTagToken.getNextSibling(); 446 447 if (nextNode == null) { 448 nextSibling = tag.getNextSibling(); 449 } 450 else if (nextNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 451 nextSibling = nextNode.getFirstChild(); 452 } 453 else { 454 nextSibling = nextNode; 455 } 456 457 if (nextSibling != null 458 && nextSibling.getType() == JavadocCommentsTokenTypes.HTML_COMMENT) { 459 nextSibling = nextSibling.getNextSibling(); 460 } 461 return nextSibling; 462 } 463 464}