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 continue; 246 } 247 htmlElement = htmlElement.getNextSibling(); 248 } 249 if (htmlElement != null 250 && JavadocUtil.findFirstToken(htmlElement, 251 JavadocCommentsTokenTypes.HTML_TAG_END) == null) { 252 htmlElement = null; 253 } 254 255 return htmlElement; 256 } 257 258 /** 259 * Finds and returns first block-level html element name. 260 * 261 * @param htmlElement block-level html tag. 262 * @return block-level html element name or null if not found. 263 */ 264 @Nullable 265 private static String getHtmlElementName(DetailNode htmlElement) { 266 final DetailNode htmlTagStart = htmlElement.getFirstChild(); 267 final DetailNode htmlTagName = 268 JavadocUtil.findFirstToken(htmlTagStart, JavadocCommentsTokenTypes.TAG_NAME); 269 String blockTagName = null; 270 if (BLOCK_TAGS.contains(htmlTagName.getText())) { 271 blockTagName = htmlTagName.getText(); 272 } 273 274 return blockTagName; 275 } 276 277 /** 278 * Returns nearest node. 279 * 280 * @param node DetailNode node. 281 * @return nearest node. 282 */ 283 private static DetailNode getNearestNode(DetailNode node) { 284 DetailNode currentNode = node; 285 while (currentNode != null 286 && (currentNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK 287 || currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE)) { 288 currentNode = currentNode.getNextSibling(); 289 } 290 if (currentNode != null 291 && currentNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 292 currentNode = currentNode.getFirstChild(); 293 } 294 return currentNode; 295 } 296 297 /** 298 * Determines whether or not the line is empty line. 299 * 300 * @param newLine NEWLINE node. 301 * @return true, if line is empty line. 302 */ 303 private static boolean isEmptyLine(DetailNode newLine) { 304 boolean result = false; 305 DetailNode previousSibling = newLine.getPreviousSibling(); 306 if (previousSibling != null && (previousSibling.getParent().getType() 307 == JavadocCommentsTokenTypes.JAVADOC_CONTENT 308 || insideNonTightHtml(previousSibling))) { 309 if (previousSibling.getType() == JavadocCommentsTokenTypes.TEXT 310 && CommonUtil.isBlank(previousSibling.getText())) { 311 previousSibling = previousSibling.getPreviousSibling(); 312 } 313 result = previousSibling != null 314 && previousSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK; 315 } 316 return result; 317 } 318 319 /** 320 * Checks whether the given node is inside a non-tight HTML element. 321 * 322 * @param previousSibling the node to check 323 * @return true if inside non-tight HTML, false otherwise 324 */ 325 private static boolean insideNonTightHtml(DetailNode previousSibling) { 326 final DetailNode parent = previousSibling.getParent(); 327 DetailNode htmlElement = parent; 328 if (parent.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 329 htmlElement = parent.getParent(); 330 } 331 return htmlElement.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT 332 && JavadocUtil.findFirstToken(htmlElement, 333 JavadocCommentsTokenTypes.HTML_TAG_END) == null; 334 } 335 336 /** 337 * Determines whether or not the line with paragraph tag is first line in javadoc. 338 * 339 * @param paragraphTag paragraph tag. 340 * @return true, if line with paragraph tag is first line in javadoc. 341 */ 342 private static boolean isFirstParagraph(DetailNode paragraphTag) { 343 boolean result = true; 344 DetailNode previousNode = paragraphTag.getPreviousSibling(); 345 while (previousNode != null) { 346 if (previousNode.getType() == JavadocCommentsTokenTypes.TEXT 347 && !CommonUtil.isBlank(previousNode.getText()) 348 || previousNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK 349 && previousNode.getType() != JavadocCommentsTokenTypes.NEWLINE 350 && previousNode.getType() != JavadocCommentsTokenTypes.TEXT) { 351 result = false; 352 break; 353 } 354 previousNode = previousNode.getPreviousSibling(); 355 } 356 return result; 357 } 358 359 /** 360 * Finds and returns nearest empty line in javadoc. 361 * 362 * @param node DetailNode node. 363 * @return Some nearest empty line in javadoc. 364 */ 365 private static DetailNode getNearestEmptyLine(DetailNode node) { 366 DetailNode newLine = node; 367 while (newLine != null) { 368 final DetailNode previousSibling = newLine.getPreviousSibling(); 369 if (newLine.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(newLine)) { 370 break; 371 } 372 newLine = previousSibling; 373 } 374 return newLine; 375 } 376 377 /** 378 * Tests whether the paragraph tag is immediately followed by the text. 379 * 380 * @param tag html tag. 381 * @return true, if the paragraph tag is immediately followed by the text. 382 */ 383 private static boolean isImmediatelyFollowedByText(DetailNode tag) { 384 final DetailNode nextSibling = getNextSibling(tag); 385 386 return nextSibling == null || nextSibling.getText().startsWith(" "); 387 } 388 389 /** 390 * Tests whether the paragraph tag is immediately followed by the new line. 391 * 392 * @param tag html tag. 393 * @return true, if the paragraph tag is immediately followed by the new line. 394 */ 395 private static boolean isImmediatelyFollowedByNewLine(DetailNode tag) { 396 final DetailNode sibling = getNextSibling(tag); 397 return sibling != null && sibling.getType() == JavadocCommentsTokenTypes.NEWLINE; 398 } 399 400 /** 401 * Custom getNextSibling method to handle different types of paragraph tag. 402 * It works for both {@code <p>} and {@code <p></p>} tags. 403 * 404 * @param tag HTML_ELEMENT tag. 405 * @return next sibling of the tag. 406 */ 407 private static DetailNode getNextSibling(DetailNode tag) { 408 DetailNode nextSibling; 409 final DetailNode paragraphStartTagToken = tag.getFirstChild(); 410 final DetailNode nextNode = paragraphStartTagToken.getNextSibling(); 411 412 if (nextNode == null) { 413 nextSibling = tag.getNextSibling(); 414 } 415 else if (nextNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) { 416 nextSibling = nextNode.getFirstChild(); 417 } 418 else { 419 nextSibling = nextNode; 420 } 421 422 if (nextSibling != null 423 && nextSibling.getType() == JavadocCommentsTokenTypes.HTML_COMMENT) { 424 nextSibling = nextSibling.getNextSibling(); 425 } 426 return nextSibling; 427 } 428}