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>},{@literal <div>},{@literal <dl>}, 048 * {@literal <h1>},{@literal <h2>},{@literal <h3>},{@literal <h4>},{@literal <h5>}, 049 * {@literal <h6>},{@literal <hr>},{@literal <ol>},{@literal <p>},{@literal <pre>}, 050 * {@literal <table>},{@literal <ul>}. 051 * </li> 052 * </ul> 053 * 054 * <p><b>ATTENTION:</b></p> 055 * 056 * <p>This Check ignores HTML comments.</p> 057 * 058 * <p>The Check ignores all the nested paragraph tags, 059 * it will not give any kind of violation if the paragraph tag is nested. 060 * It also ignores paragraph tags inside block tags.</p> 061 * 062 * @since 6.0 063 */ 064@StatelessCheck 065public class JavadocParagraphCheck extends AbstractJavadocCheck { 066 067 /** 068 * A key is pointing to the warning message text in "messages.properties" file. 069 */ 070 public static final String MSG_TAG_AFTER = "javadoc.paragraph.tag.after"; 071 072 /** 073 * A key is pointing to the warning message text in "messages.properties" file. 074 */ 075 public static final String MSG_LINE_BEFORE = "javadoc.paragraph.line.before"; 076 077 /** 078 * A key is pointing to the warning message text in "messages.properties" file. 079 */ 080 public static final String MSG_REDUNDANT_PARAGRAPH = "javadoc.paragraph.redundant.paragraph"; 081 082 /** 083 * A key is pointing to the warning message text in "messages.properties" file. 084 */ 085 public static final String MSG_MISPLACED_TAG = "javadoc.paragraph.misplaced.tag"; 086 087 /** 088 * A key is pointing to the warning message text in "messages.properties" file. 089 */ 090 public static final String MSG_PRECEDED_BLOCK_TAG = "javadoc.paragraph.preceded.block.tag"; 091 092 /** 093 * Constant for the paragraph tag name. 094 */ 095 private static final String PARAGRAPH_TAG = "p"; 096 097 /** 098 * Set of block tags supported by this check. 099 */ 100 private static final Set<String> BLOCK_TAGS = 101 Set.of("address", "blockquote", "div", "dl", 102 "h1", "h2", "h3", "h4", "h5", "h6", "hr", 103 "ol", PARAGRAPH_TAG, "pre", "table", "ul"); 104 105 /** 106 * Control whether the {@literal <p>} tag should be placed immediately before the first word. 107 */ 108 private boolean allowNewlineParagraph = true; 109 110 /** 111 * Creates a new {@code JavadocParagraphCheck} instance. 112 */ 113 public JavadocParagraphCheck() { 114 // no code by default 115 } 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, 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, MSG_REDUNDANT_PARAGRAPH); 174 } 175 else if (newLine == null || tag.getLineNumber() - newLine.getLineNumber() != 1) { 176 log(tag, MSG_LINE_BEFORE); 177 } 178 179 final String blockTagName = findFollowedBlockTagName(tag); 180 if (blockTagName != null) { 181 log(tag, MSG_PRECEDED_BLOCK_TAG, blockTagName); 182 } 183 184 if (!allowNewlineParagraph && isImmediatelyFollowedByNewLine(tag)) { 185 log(tag, MSG_MISPLACED_TAG); 186 } 187 if (isImmediatelyFollowedByText(tag)) { 188 log(tag, MSG_MISPLACED_TAG); 189 } 190 } 191 } 192 193 /** 194 * Determines whether the paragraph tag is nested. 195 * 196 * @param tag html tag. 197 * @return true, if the paragraph tag is nested. 198 */ 199 private static boolean isNestedParagraph(DetailNode tag) { 200 boolean nested = false; 201 DetailNode parent = tag.getParent(); 202 203 while (parent != null) { 204 if (parent.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) { 205 nested = true; 206 break; 207 } 208 parent = parent.getParent(); 209 } 210 211 return nested; 212 } 213 214 /** 215 * Determines whether the paragraph tag is inside javadoc block tag. 216 * 217 * @param tag html tag. 218 * @return true, if the paragraph tag is inside javadoc block tag. 219 */ 220 private static boolean isInsideBlockTag(DetailNode tag) { 221 boolean result = false; 222 DetailNode parent = tag; 223 224 while (parent != null) { 225 if (parent.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) { 226 result = true; 227 break; 228 } 229 parent = parent.getParent(); 230 } 231 232 return result; 233 } 234 235 /** 236 * Determines whether or not the paragraph tag is followed by block tag. 237 * 238 * @param tag html tag. 239 * @return block tag if the paragraph tag is followed by block tag or null if not found. 240 */ 241 @Nullable 242 private static String findFollowedBlockTagName(DetailNode tag) { 243 final DetailNode htmlElement = findFirstHtmlElementAfter(tag); 244 String blockTagName = null; 245 246 if (htmlElement != null) { 247 blockTagName = getHtmlElementName(htmlElement); 248 } 249 250 return blockTagName; 251 } 252 253 /** 254 * Finds and returns first html element after the tag. 255 * 256 * @param tag html tag. 257 * @return first html element after the paragraph tag or null if not found. 258 */ 259 @Nullable 260 private static DetailNode findFirstHtmlElementAfter(DetailNode tag) { 261 DetailNode htmlElement = getNextSibling(tag); 262 263 while (htmlElement != null 264 && htmlElement.getType() != JavadocCommentsTokenTypes.HTML_ELEMENT) { 265 if (htmlElement.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 266 htmlElement = htmlElement.getFirstChild(); 267 } 268 else if (htmlElement.getType() == JavadocCommentsTokenTypes.TEXT 269 && !CommonUtil.isBlank(htmlElement.getText())) { 270 htmlElement = null; 271 break; 272 } 273 else { 274 htmlElement = htmlElement.getNextSibling(); 275 } 276 } 277 if (htmlElement != null 278 && JavadocUtil.findFirstToken(htmlElement, 279 JavadocCommentsTokenTypes.HTML_TAG_END) == null) { 280 htmlElement = null; 281 } 282 283 return htmlElement; 284 } 285 286 /** 287 * Finds and returns first block-level html element name. 288 * 289 * @param htmlElement block-level html tag. 290 * @return block-level html element name or null if not found. 291 */ 292 @Nullable 293 private static String getHtmlElementName(DetailNode htmlElement) { 294 final DetailNode htmlTagStart = htmlElement.getFirstChild(); 295 final DetailNode htmlTagName = 296 JavadocUtil.findFirstToken(htmlTagStart, JavadocCommentsTokenTypes.TAG_NAME); 297 String blockTagName = null; 298 if (BLOCK_TAGS.contains(htmlTagName.getText())) { 299 blockTagName = htmlTagName.getText(); 300 } 301 302 return blockTagName; 303 } 304 305 /** 306 * Returns nearest node. 307 * 308 * @param node DetailNode node. 309 * @return nearest node. 310 */ 311 private static DetailNode getNearestNode(DetailNode node) { 312 DetailNode currentNode = node; 313 while (currentNode != null 314 && (currentNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK 315 || currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE)) { 316 currentNode = currentNode.getNextSibling(); 317 } 318 if (currentNode != null 319 && currentNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 320 currentNode = currentNode.getFirstChild(); 321 } 322 return currentNode; 323 } 324 325 /** 326 * Determines whether or not the line is empty line. 327 * 328 * @param newLine NEWLINE node. 329 * @return true, if line is empty line. 330 */ 331 private static boolean isEmptyLine(DetailNode newLine) { 332 boolean result = false; 333 DetailNode previousSibling = newLine.getPreviousSibling(); 334 if (previousSibling != null && (previousSibling.getParent().getType() 335 == JavadocCommentsTokenTypes.JAVADOC_CONTENT 336 || insideNonTightHtml(previousSibling))) { 337 if (previousSibling.getType() == JavadocCommentsTokenTypes.TEXT 338 && CommonUtil.isBlank(previousSibling.getText())) { 339 previousSibling = previousSibling.getPreviousSibling(); 340 } 341 result = previousSibling != null 342 && previousSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK; 343 } 344 return result; 345 } 346 347 /** 348 * Checks whether the given node is inside a non-tight HTML element. 349 * 350 * @param previousSibling the node to check 351 * @return true if inside non-tight HTML, false otherwise 352 */ 353 private static boolean insideNonTightHtml(DetailNode previousSibling) { 354 final DetailNode parent = previousSibling.getParent(); 355 DetailNode htmlElement = parent; 356 if (parent.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 357 htmlElement = parent.getParent(); 358 } 359 return htmlElement.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT 360 && JavadocUtil.findFirstToken(htmlElement, 361 JavadocCommentsTokenTypes.HTML_TAG_END) == null; 362 } 363 364 /** 365 * Determines whether or not the line with paragraph tag is first line in javadoc. 366 * 367 * @param paragraphTag paragraph tag. 368 * @return true, if line with paragraph tag is first line in javadoc. 369 */ 370 private static boolean isFirstParagraph(DetailNode paragraphTag) { 371 boolean result = true; 372 DetailNode previousNode = paragraphTag.getPreviousSibling(); 373 while (previousNode != null) { 374 if (previousNode.getType() == JavadocCommentsTokenTypes.TEXT 375 && !CommonUtil.isBlank(previousNode.getText()) 376 || previousNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK 377 && previousNode.getType() != JavadocCommentsTokenTypes.NEWLINE 378 && previousNode.getType() != JavadocCommentsTokenTypes.TEXT) { 379 result = false; 380 break; 381 } 382 previousNode = previousNode.getPreviousSibling(); 383 } 384 return result; 385 } 386 387 /** 388 * Finds and returns nearest empty line in javadoc. 389 * 390 * @param node DetailNode node. 391 * @return Some nearest empty line in javadoc. 392 */ 393 private static DetailNode getNearestEmptyLine(DetailNode node) { 394 DetailNode newLine = node; 395 while (newLine != null) { 396 final DetailNode previousSibling = newLine.getPreviousSibling(); 397 if (newLine.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(newLine)) { 398 break; 399 } 400 newLine = previousSibling; 401 } 402 return newLine; 403 } 404 405 /** 406 * Tests whether the paragraph tag is immediately followed by the text. 407 * 408 * @param tag html tag. 409 * @return true, if the paragraph tag is immediately followed by the text. 410 */ 411 private static boolean isImmediatelyFollowedByText(DetailNode tag) { 412 final DetailNode nextSibling = getNextSibling(tag); 413 414 return nextSibling == null || nextSibling.getText().startsWith(" "); 415 } 416 417 /** 418 * Tests whether the paragraph tag is immediately followed by the new line. 419 * 420 * @param tag html tag. 421 * @return true, if the paragraph tag is immediately followed by the new line. 422 */ 423 private static boolean isImmediatelyFollowedByNewLine(DetailNode tag) { 424 final DetailNode sibling = getNextSibling(tag); 425 return sibling != null && sibling.getType() == JavadocCommentsTokenTypes.NEWLINE; 426 } 427 428 /** 429 * Custom getNextSibling method to handle different types of paragraph tag. 430 * It works for both {@code <p>} and {@code <p></p>} tags. 431 * 432 * @param tag HTML_ELEMENT tag. 433 * @return next sibling of the tag. 434 */ 435 private static DetailNode getNextSibling(DetailNode tag) { 436 DetailNode nextSibling; 437 final DetailNode paragraphStartTagToken = tag.getFirstChild(); 438 final DetailNode nextNode = paragraphStartTagToken.getNextSibling(); 439 440 if (nextNode == null) { 441 nextSibling = tag.getNextSibling(); 442 } 443 else if (nextNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 444 nextSibling = nextNode.getFirstChild(); 445 } 446 else { 447 nextSibling = nextNode; 448 } 449 450 if (nextSibling != null 451 && nextSibling.getType() == JavadocCommentsTokenTypes.HTML_COMMENT) { 452 nextSibling = nextSibling.getNextSibling(); 453 } 454 return nextSibling; 455 } 456 457}