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