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 * Setter to control whether the {@literal <p>} tag should be placed 119 * immediately before the first word. 120 * 121 * @param value value to set. 122 * @since 6.9 123 */ 124 public void setAllowNewlineParagraph(boolean value) { 125 allowNewlineParagraph = value; 126 } 127 128 @Override 129 public int[] getDefaultJavadocTokens() { 130 return new int[] { 131 JavadocCommentsTokenTypes.NEWLINE, 132 JavadocCommentsTokenTypes.HTML_ELEMENT, 133 }; 134 } 135 136 @Override 137 public int[] getRequiredJavadocTokens() { 138 return getAcceptableJavadocTokens(); 139 } 140 141 @Override 142 public void visitJavadocToken(DetailNode ast) { 143 if (ast.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(ast)) { 144 checkEmptyLine(ast); 145 } 146 else if (JavadocUtil.isTag(ast, PARAGRAPH_TAG)) { 147 checkParagraphTag(ast); 148 } 149 } 150 151 /** 152 * Determines whether or not the next line after empty line has paragraph tag in the beginning. 153 * 154 * @param newline NEWLINE node. 155 */ 156 private void checkEmptyLine(DetailNode newline) { 157 final DetailNode nearestToken = getNearestNode(newline); 158 if (nearestToken != null && nearestToken.getType() == JavadocCommentsTokenTypes.TEXT 159 && !CommonUtil.isBlank(nearestToken.getText())) { 160 log(newline.getLineNumber(), newline.getColumnNumber(), MSG_TAG_AFTER); 161 } 162 } 163 164 /** 165 * Determines whether or not the line with paragraph tag has previous empty line. 166 * 167 * @param tag html tag. 168 */ 169 private void checkParagraphTag(DetailNode tag) { 170 if (!isNestedParagraph(tag) && !isInsideBlockTag(tag)) { 171 final DetailNode newLine = getNearestEmptyLine(tag); 172 if (isFirstParagraph(tag)) { 173 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_REDUNDANT_PARAGRAPH); 174 } 175 else if (newLine == null || tag.getLineNumber() - newLine.getLineNumber() != 1) { 176 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_LINE_BEFORE); 177 } 178 179 final String blockTagName = findFollowedBlockTagName(tag); 180 if (blockTagName != null) { 181 log(tag.getLineNumber(), tag.getColumnNumber(), 182 MSG_PRECEDED_BLOCK_TAG, blockTagName); 183 } 184 185 if (!allowNewlineParagraph && isImmediatelyFollowedByNewLine(tag)) { 186 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_MISPLACED_TAG); 187 } 188 if (isImmediatelyFollowedByText(tag)) { 189 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_MISPLACED_TAG); 190 } 191 } 192 } 193 194 /** 195 * Determines whether the paragraph tag is nested. 196 * 197 * @param tag html tag. 198 * @return true, if the paragraph tag is nested. 199 */ 200 private static boolean isNestedParagraph(DetailNode tag) { 201 boolean nested = false; 202 DetailNode parent = tag.getParent(); 203 204 while (parent != null) { 205 if (parent.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) { 206 nested = true; 207 break; 208 } 209 parent = parent.getParent(); 210 } 211 212 return nested; 213 } 214 215 /** 216 * Determines whether the paragraph tag is inside javadoc block tag. 217 * 218 * @param tag html tag. 219 * @return true, if the paragraph tag is inside javadoc block tag. 220 */ 221 private static boolean isInsideBlockTag(DetailNode tag) { 222 boolean result = false; 223 DetailNode parent = tag; 224 225 while (parent != null) { 226 if (parent.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) { 227 result = true; 228 break; 229 } 230 parent = parent.getParent(); 231 } 232 233 return result; 234 } 235 236 /** 237 * Determines whether or not the paragraph tag is followed by block tag. 238 * 239 * @param tag html tag. 240 * @return block tag if the paragraph tag is followed by block tag or null if not found. 241 */ 242 @Nullable 243 private static String findFollowedBlockTagName(DetailNode tag) { 244 final DetailNode htmlElement = findFirstHtmlElementAfter(tag); 245 String blockTagName = null; 246 247 if (htmlElement != null) { 248 blockTagName = getHtmlElementName(htmlElement); 249 } 250 251 return blockTagName; 252 } 253 254 /** 255 * Finds and returns first html element after the tag. 256 * 257 * @param tag html tag. 258 * @return first html element after the paragraph tag or null if not found. 259 */ 260 @Nullable 261 private static DetailNode findFirstHtmlElementAfter(DetailNode tag) { 262 DetailNode htmlElement = getNextSibling(tag); 263 264 while (htmlElement != null 265 && htmlElement.getType() != JavadocCommentsTokenTypes.HTML_ELEMENT) { 266 if (htmlElement.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 267 htmlElement = htmlElement.getFirstChild(); 268 } 269 else if (htmlElement.getType() == JavadocCommentsTokenTypes.TEXT 270 && !CommonUtil.isBlank(htmlElement.getText())) { 271 htmlElement = null; 272 break; 273 } 274 else { 275 htmlElement = htmlElement.getNextSibling(); 276 } 277 } 278 if (htmlElement != null 279 && JavadocUtil.findFirstToken(htmlElement, 280 JavadocCommentsTokenTypes.HTML_TAG_END) == null) { 281 htmlElement = null; 282 } 283 284 return htmlElement; 285 } 286 287 /** 288 * Finds and returns first block-level html element name. 289 * 290 * @param htmlElement block-level html tag. 291 * @return block-level html element name or null if not found. 292 */ 293 @Nullable 294 private static String getHtmlElementName(DetailNode htmlElement) { 295 final DetailNode htmlTagStart = htmlElement.getFirstChild(); 296 final DetailNode htmlTagName = 297 JavadocUtil.findFirstToken(htmlTagStart, JavadocCommentsTokenTypes.TAG_NAME); 298 String blockTagName = null; 299 if (BLOCK_TAGS.contains(htmlTagName.getText())) { 300 blockTagName = htmlTagName.getText(); 301 } 302 303 return blockTagName; 304 } 305 306 /** 307 * Returns nearest node. 308 * 309 * @param node DetailNode node. 310 * @return nearest node. 311 */ 312 private static DetailNode getNearestNode(DetailNode node) { 313 DetailNode currentNode = node; 314 while (currentNode != null 315 && (currentNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK 316 || currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE)) { 317 currentNode = currentNode.getNextSibling(); 318 } 319 if (currentNode != null 320 && currentNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 321 currentNode = currentNode.getFirstChild(); 322 } 323 return currentNode; 324 } 325 326 /** 327 * Determines whether or not the line is empty line. 328 * 329 * @param newLine NEWLINE node. 330 * @return true, if line is empty line. 331 */ 332 private static boolean isEmptyLine(DetailNode newLine) { 333 boolean result = false; 334 DetailNode previousSibling = newLine.getPreviousSibling(); 335 if (previousSibling != null && (previousSibling.getParent().getType() 336 == JavadocCommentsTokenTypes.JAVADOC_CONTENT 337 || insideNonTightHtml(previousSibling))) { 338 if (previousSibling.getType() == JavadocCommentsTokenTypes.TEXT 339 && CommonUtil.isBlank(previousSibling.getText())) { 340 previousSibling = previousSibling.getPreviousSibling(); 341 } 342 result = previousSibling != null 343 && previousSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK; 344 } 345 return result; 346 } 347 348 /** 349 * Checks whether the given node is inside a non-tight HTML element. 350 * 351 * @param previousSibling the node to check 352 * @return true if inside non-tight HTML, false otherwise 353 */ 354 private static boolean insideNonTightHtml(DetailNode previousSibling) { 355 final DetailNode parent = previousSibling.getParent(); 356 DetailNode htmlElement = parent; 357 if (parent.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 358 htmlElement = parent.getParent(); 359 } 360 return htmlElement.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT 361 && JavadocUtil.findFirstToken(htmlElement, 362 JavadocCommentsTokenTypes.HTML_TAG_END) == null; 363 } 364 365 /** 366 * Determines whether or not the line with paragraph tag is first line in javadoc. 367 * 368 * @param paragraphTag paragraph tag. 369 * @return true, if line with paragraph tag is first line in javadoc. 370 */ 371 private static boolean isFirstParagraph(DetailNode paragraphTag) { 372 boolean result = true; 373 DetailNode previousNode = paragraphTag.getPreviousSibling(); 374 while (previousNode != null) { 375 if (previousNode.getType() == JavadocCommentsTokenTypes.TEXT 376 && !CommonUtil.isBlank(previousNode.getText()) 377 || previousNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK 378 && previousNode.getType() != JavadocCommentsTokenTypes.NEWLINE 379 && previousNode.getType() != JavadocCommentsTokenTypes.TEXT) { 380 result = false; 381 break; 382 } 383 previousNode = previousNode.getPreviousSibling(); 384 } 385 return result; 386 } 387 388 /** 389 * Finds and returns nearest empty line in javadoc. 390 * 391 * @param node DetailNode node. 392 * @return Some nearest empty line in javadoc. 393 */ 394 private static DetailNode getNearestEmptyLine(DetailNode node) { 395 DetailNode newLine = node; 396 while (newLine != null) { 397 final DetailNode previousSibling = newLine.getPreviousSibling(); 398 if (newLine.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(newLine)) { 399 break; 400 } 401 newLine = previousSibling; 402 } 403 return newLine; 404 } 405 406 /** 407 * Tests whether the paragraph tag is immediately followed by the text. 408 * 409 * @param tag html tag. 410 * @return true, if the paragraph tag is immediately followed by the text. 411 */ 412 private static boolean isImmediatelyFollowedByText(DetailNode tag) { 413 final DetailNode nextSibling = getNextSibling(tag); 414 415 return nextSibling == null || nextSibling.getText().startsWith(" "); 416 } 417 418 /** 419 * Tests whether the paragraph tag is immediately followed by the new line. 420 * 421 * @param tag html tag. 422 * @return true, if the paragraph tag is immediately followed by the new line. 423 */ 424 private static boolean isImmediatelyFollowedByNewLine(DetailNode tag) { 425 final DetailNode sibling = getNextSibling(tag); 426 return sibling != null && sibling.getType() == JavadocCommentsTokenTypes.NEWLINE; 427 } 428 429 /** 430 * Custom getNextSibling method to handle different types of paragraph tag. 431 * It works for both {@code <p>} and {@code <p></p>} tags. 432 * 433 * @param tag HTML_ELEMENT tag. 434 * @return next sibling of the tag. 435 */ 436 private static DetailNode getNextSibling(DetailNode tag) { 437 DetailNode nextSibling; 438 final DetailNode paragraphStartTagToken = tag.getFirstChild(); 439 final DetailNode nextNode = paragraphStartTagToken.getNextSibling(); 440 441 if (nextNode == null) { 442 nextSibling = tag.getNextSibling(); 443 } 444 else if (nextNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 445 nextSibling = nextNode.getFirstChild(); 446 } 447 else { 448 nextSibling = nextNode; 449 } 450 451 if (nextSibling != null 452 && nextSibling.getType() == JavadocCommentsTokenTypes.HTML_COMMENT) { 453 nextSibling = nextSibling.getNextSibling(); 454 } 455 return nextSibling; 456 } 457}