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 <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. 061 * It also ignores paragraph tags inside block tags.</p> 062 * 063 * @since 6.0 064 */ 065@StatelessCheck 066public class JavadocParagraphCheck extends AbstractJavadocCheck { 067 068 /** 069 * A key is pointing to the warning message text in "messages.properties" 070 * file. 071 */ 072 public static final String MSG_TAG_AFTER = "javadoc.paragraph.tag.after"; 073 074 /** 075 * A key is pointing to the warning message text in "messages.properties" 076 * file. 077 */ 078 public static final String MSG_LINE_BEFORE = "javadoc.paragraph.line.before"; 079 080 /** 081 * A key is pointing to the warning message text in "messages.properties" 082 * file. 083 */ 084 public static final String MSG_REDUNDANT_PARAGRAPH = "javadoc.paragraph.redundant.paragraph"; 085 086 /** 087 * A key is pointing to the warning message text in "messages.properties" 088 * file. 089 */ 090 public static final String MSG_MISPLACED_TAG = "javadoc.paragraph.misplaced.tag"; 091 092 /** 093 * A key is pointing to the warning message text in "messages.properties" 094 * file. 095 */ 096 public static final String MSG_PRECEDED_BLOCK_TAG = "javadoc.paragraph.preceded.block.tag"; 097 098 /** 099 * Constant for the paragraph tag name. 100 */ 101 private static final String PARAGRAPH_TAG = "p"; 102 103 /** 104 * Set of block tags supported by this check. 105 */ 106 private static final Set<String> BLOCK_TAGS = 107 Set.of("address", "blockquote", "div", "dl", 108 "h1", "h2", "h3", "h4", "h5", "h6", "hr", 109 "ol", PARAGRAPH_TAG, "pre", "table", "ul"); 110 111 /** 112 * Control whether the <p> tag should be placed immediately before the first word. 113 */ 114 private boolean allowNewlineParagraph = true; 115 116 /** 117 * Setter to control whether the <p> tag should be placed 118 * immediately before the first word. 119 * 120 * @param value value to set. 121 * @since 6.9 122 */ 123 public void setAllowNewlineParagraph(boolean value) { 124 allowNewlineParagraph = value; 125 } 126 127 @Override 128 public int[] getDefaultJavadocTokens() { 129 return new int[] { 130 JavadocCommentsTokenTypes.NEWLINE, 131 JavadocCommentsTokenTypes.HTML_ELEMENT, 132 }; 133 } 134 135 @Override 136 public int[] getRequiredJavadocTokens() { 137 return getAcceptableJavadocTokens(); 138 } 139 140 @Override 141 public void visitJavadocToken(DetailNode ast) { 142 if (ast.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(ast)) { 143 checkEmptyLine(ast); 144 } 145 else if (JavadocUtil.isTag(ast, PARAGRAPH_TAG)) { 146 checkParagraphTag(ast); 147 } 148 } 149 150 /** 151 * Determines whether or not the next line after empty line has paragraph tag in the beginning. 152 * 153 * @param newline NEWLINE node. 154 */ 155 private void checkEmptyLine(DetailNode newline) { 156 final DetailNode nearestToken = getNearestNode(newline); 157 if (nearestToken != null && nearestToken.getType() == JavadocCommentsTokenTypes.TEXT 158 && !CommonUtil.isBlank(nearestToken.getText())) { 159 log(newline.getLineNumber(), newline.getColumnNumber(), MSG_TAG_AFTER); 160 } 161 } 162 163 /** 164 * Determines whether or not the line with paragraph tag has previous empty line. 165 * 166 * @param tag html tag. 167 */ 168 private void checkParagraphTag(DetailNode tag) { 169 if (!isNestedParagraph(tag) && !isInsideBlockTag(tag)) { 170 final DetailNode newLine = getNearestEmptyLine(tag); 171 if (isFirstParagraph(tag)) { 172 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_REDUNDANT_PARAGRAPH); 173 } 174 else if (newLine == null || tag.getLineNumber() - newLine.getLineNumber() != 1) { 175 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_LINE_BEFORE); 176 } 177 178 final String blockTagName = findFollowedBlockTagName(tag); 179 if (blockTagName != null) { 180 log(tag.getLineNumber(), tag.getColumnNumber(), 181 MSG_PRECEDED_BLOCK_TAG, blockTagName); 182 } 183 184 if (!allowNewlineParagraph && isImmediatelyFollowedByNewLine(tag)) { 185 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_MISPLACED_TAG); 186 } 187 if (isImmediatelyFollowedByText(tag)) { 188 log(tag.getLineNumber(), tag.getColumnNumber(), 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}