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