View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.Set;
23  
24  import javax.annotation.Nullable;
25  
26  import com.puppycrawl.tools.checkstyle.StatelessCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailNode;
28  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
31  
32  /**
33   * <div>
34   * Checks the Javadoc paragraph.
35   * </div>
36   *
37   * <p>
38   * Checks that:
39   * </p>
40   * <ul>
41   * <li>There is one blank line between each of two paragraphs.</li>
42   * <li>Each paragraph but the first has &lt;p&gt; immediately
43   * before the first word, with no space after.</li>
44   * <li>The outer most paragraph tags should not precede
45   * <a href="https://www.w3schools.com/html/html_blocks.asp">HTML block-tag</a>.
46   * Nested paragraph tags are allowed to do that. This check only supports following block-tags:
47   * &lt;address&gt;,&lt;blockquote&gt;
48   * ,&lt;div&gt;,&lt;dl&gt;
49   * ,&lt;h1&gt;,&lt;h2&gt;,&lt;h3&gt;,&lt;h4&gt;,&lt;h5&gt;,&lt;h6&gt;,&lt;hr&gt;
50   * ,&lt;ol&gt;,&lt;p&gt;,&lt;pre&gt;
51   * ,&lt;table&gt;,&lt;ul&gt;.
52   * </li>
53   * </ul>
54   *
55   * <p><b>ATTENTION:</b></p>
56   *
57   * <p>This Check ignores HTML comments.</p>
58   *
59   * <p>The Check ignores all the nested paragraph tags,
60   * it will not give any kind of violation if the paragraph tag is nested.
61   * It also ignores paragraph tags inside block tags.</p>
62   *
63   * @since 6.0
64   */
65  @StatelessCheck
66  public class JavadocParagraphCheck extends AbstractJavadocCheck {
67  
68      /**
69       * A key is pointing to the warning message text in "messages.properties"
70       * file.
71       */
72      public static final String MSG_TAG_AFTER = "javadoc.paragraph.tag.after";
73  
74      /**
75       * A key is pointing to the warning message text in "messages.properties"
76       * file.
77       */
78      public static final String MSG_LINE_BEFORE = "javadoc.paragraph.line.before";
79  
80      /**
81       * A key is pointing to the warning message text in "messages.properties"
82       * file.
83       */
84      public static final String MSG_REDUNDANT_PARAGRAPH = "javadoc.paragraph.redundant.paragraph";
85  
86      /**
87       * A key is pointing to the warning message text in "messages.properties"
88       * file.
89       */
90      public static final String MSG_MISPLACED_TAG = "javadoc.paragraph.misplaced.tag";
91  
92      /**
93       * A key is pointing to the warning message text in "messages.properties"
94       * file.
95       */
96      public static final String MSG_PRECEDED_BLOCK_TAG = "javadoc.paragraph.preceded.block.tag";
97  
98      /**
99       * 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 &lt;p&gt; tag should be placed immediately before the first word.
113      */
114     private boolean allowNewlineParagraph = true;
115 
116     /**
117      * Setter to control whether the &lt;p&gt; 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 }