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