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