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 {@literal <p>} 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   * {@literal <address>},{@literal <blockquote>}
48   * ,{@literal <div>},{@literal <dl>}
49   * ,{@literal <h1>},{@literal <h2>},{@literal <h3>},{@literal <h4>},{@literal <h5>},
50   * {@literal <h6>},{@literal <hr>}
51   * ,{@literal <ol>},{@literal <p>},{@literal <pre>}
52   * ,{@literal <table>},{@literal <ul>}.
53   * </li>
54   * </ul>
55   *
56   * <p><b>ATTENTION:</b></p>
57   *
58   * <p>This Check ignores HTML comments.</p>
59   *
60   * <p>The Check ignores all the nested paragraph tags,
61   * it will not give any kind of violation if the paragraph tag is nested.
62   * It also ignores paragraph tags inside block tags.</p>
63   *
64   * @since 6.0
65   */
66  @StatelessCheck
67  public class JavadocParagraphCheck extends AbstractJavadocCheck {
68  
69      /**
70       * A key is pointing to the warning message text in "messages.properties"
71       * file.
72       */
73      public static final String MSG_TAG_AFTER = "javadoc.paragraph.tag.after";
74  
75      /**
76       * A key is pointing to the warning message text in "messages.properties"
77       * file.
78       */
79      public static final String MSG_LINE_BEFORE = "javadoc.paragraph.line.before";
80  
81      /**
82       * A key is pointing to the warning message text in "messages.properties"
83       * file.
84       */
85      public static final String MSG_REDUNDANT_PARAGRAPH = "javadoc.paragraph.redundant.paragraph";
86  
87      /**
88       * A key is pointing to the warning message text in "messages.properties"
89       * file.
90       */
91      public static final String MSG_MISPLACED_TAG = "javadoc.paragraph.misplaced.tag";
92  
93      /**
94       * A key is pointing to the warning message text in "messages.properties"
95       * file.
96       */
97      public static final String MSG_PRECEDED_BLOCK_TAG = "javadoc.paragraph.preceded.block.tag";
98  
99      /**
100      * Constant for the paragraph tag name.
101      */
102     private static final String PARAGRAPH_TAG = "p";
103 
104     /**
105      * Set of block tags supported by this check.
106      */
107     private static final Set<String> BLOCK_TAGS =
108             Set.of("address", "blockquote", "div", "dl",
109                    "h1", "h2", "h3", "h4", "h5", "h6", "hr",
110                    "ol", PARAGRAPH_TAG, "pre", "table", "ul");
111 
112     /**
113      * Control whether the {@literal <p>} tag should be placed immediately before the first word.
114      */
115     private boolean allowNewlineParagraph = true;
116 
117     /**
118      * Setter to control whether the {@literal <p>} tag should be placed
119      * immediately before the first word.
120      *
121      * @param value value to set.
122      * @since 6.9
123      */
124     public void setAllowNewlineParagraph(boolean value) {
125         allowNewlineParagraph = value;
126     }
127 
128     @Override
129     public int[] getDefaultJavadocTokens() {
130         return new int[] {
131             JavadocCommentsTokenTypes.NEWLINE,
132             JavadocCommentsTokenTypes.HTML_ELEMENT,
133         };
134     }
135 
136     @Override
137     public int[] getRequiredJavadocTokens() {
138         return getAcceptableJavadocTokens();
139     }
140 
141     @Override
142     public void visitJavadocToken(DetailNode ast) {
143         if (ast.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(ast)) {
144             checkEmptyLine(ast);
145         }
146         else if (JavadocUtil.isTag(ast, PARAGRAPH_TAG)) {
147             checkParagraphTag(ast);
148         }
149     }
150 
151     /**
152      * Determines whether or not the next line after empty line has paragraph tag in the beginning.
153      *
154      * @param newline NEWLINE node.
155      */
156     private void checkEmptyLine(DetailNode newline) {
157         final DetailNode nearestToken = getNearestNode(newline);
158         if (nearestToken != null && nearestToken.getType() == JavadocCommentsTokenTypes.TEXT
159                 && !CommonUtil.isBlank(nearestToken.getText())) {
160             log(newline.getLineNumber(), newline.getColumnNumber(), MSG_TAG_AFTER);
161         }
162     }
163 
164     /**
165      * Determines whether or not the line with paragraph tag has previous empty line.
166      *
167      * @param tag html tag.
168      */
169     private void checkParagraphTag(DetailNode tag) {
170         if (!isNestedParagraph(tag) && !isInsideBlockTag(tag)) {
171             final DetailNode newLine = getNearestEmptyLine(tag);
172             if (isFirstParagraph(tag)) {
173                 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_REDUNDANT_PARAGRAPH);
174             }
175             else if (newLine == null || tag.getLineNumber() - newLine.getLineNumber() != 1) {
176                 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_LINE_BEFORE);
177             }
178 
179             final String blockTagName = findFollowedBlockTagName(tag);
180             if (blockTagName != null) {
181                 log(tag.getLineNumber(), tag.getColumnNumber(),
182                         MSG_PRECEDED_BLOCK_TAG, blockTagName);
183             }
184 
185             if (!allowNewlineParagraph && isImmediatelyFollowedByNewLine(tag)) {
186                 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_MISPLACED_TAG);
187             }
188             if (isImmediatelyFollowedByText(tag)) {
189                 log(tag.getLineNumber(), tag.getColumnNumber(), MSG_MISPLACED_TAG);
190             }
191         }
192     }
193 
194     /**
195      * Determines whether the paragraph tag is nested.
196      *
197      * @param tag html tag.
198      * @return true, if the paragraph tag is nested.
199      */
200     private static boolean isNestedParagraph(DetailNode tag) {
201         boolean nested = false;
202         DetailNode parent = tag.getParent();
203 
204         while (parent != null) {
205             if (parent.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
206                 nested = true;
207                 break;
208             }
209             parent = parent.getParent();
210         }
211 
212         return nested;
213     }
214 
215     /**
216      * Determines whether the paragraph tag is inside javadoc block tag.
217      *
218      * @param tag html tag.
219      * @return true, if the paragraph tag is inside javadoc block tag.
220      */
221     private static boolean isInsideBlockTag(DetailNode tag) {
222         boolean result = false;
223         DetailNode parent = tag;
224 
225         while (parent != null) {
226             if (parent.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
227                 result = true;
228                 break;
229             }
230             parent = parent.getParent();
231         }
232 
233         return result;
234     }
235 
236     /**
237      * Determines whether or not the paragraph tag is followed by block tag.
238      *
239      * @param tag html tag.
240      * @return block tag if the paragraph tag is followed by block tag or null if not found.
241      */
242     @Nullable
243     private static String findFollowedBlockTagName(DetailNode tag) {
244         final DetailNode htmlElement = findFirstHtmlElementAfter(tag);
245         String blockTagName = null;
246 
247         if (htmlElement != null) {
248             blockTagName = getHtmlElementName(htmlElement);
249         }
250 
251         return blockTagName;
252     }
253 
254     /**
255      * Finds and returns first html element after the tag.
256      *
257      * @param tag html tag.
258      * @return first html element after the paragraph tag or null if not found.
259      */
260     @Nullable
261     private static DetailNode findFirstHtmlElementAfter(DetailNode tag) {
262         DetailNode htmlElement = getNextSibling(tag);
263 
264         while (htmlElement != null
265                 && htmlElement.getType() != JavadocCommentsTokenTypes.HTML_ELEMENT) {
266             if (htmlElement.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
267                 htmlElement = htmlElement.getFirstChild();
268             }
269             else if (htmlElement.getType() == JavadocCommentsTokenTypes.TEXT
270                     && !CommonUtil.isBlank(htmlElement.getText())) {
271                 htmlElement = null;
272                 break;
273             }
274             else {
275                 htmlElement = htmlElement.getNextSibling();
276             }
277         }
278         if (htmlElement != null
279                 && JavadocUtil.findFirstToken(htmlElement,
280                         JavadocCommentsTokenTypes.HTML_TAG_END) == null) {
281             htmlElement = null;
282         }
283 
284         return htmlElement;
285     }
286 
287     /**
288      * Finds and returns first block-level html element name.
289      *
290      * @param htmlElement block-level html tag.
291      * @return block-level html element name or null if not found.
292      */
293     @Nullable
294     private static String getHtmlElementName(DetailNode htmlElement) {
295         final DetailNode htmlTagStart = htmlElement.getFirstChild();
296         final DetailNode htmlTagName =
297                 JavadocUtil.findFirstToken(htmlTagStart, JavadocCommentsTokenTypes.TAG_NAME);
298         String blockTagName = null;
299         if (BLOCK_TAGS.contains(htmlTagName.getText())) {
300             blockTagName = htmlTagName.getText();
301         }
302 
303         return blockTagName;
304     }
305 
306     /**
307      * Returns nearest node.
308      *
309      * @param node DetailNode node.
310      * @return nearest node.
311      */
312     private static DetailNode getNearestNode(DetailNode node) {
313         DetailNode currentNode = node;
314         while (currentNode != null
315                 && (currentNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
316                     || currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE)) {
317             currentNode = currentNode.getNextSibling();
318         }
319         if (currentNode != null
320                 && currentNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
321             currentNode = currentNode.getFirstChild();
322         }
323         return currentNode;
324     }
325 
326     /**
327      * Determines whether or not the line is empty line.
328      *
329      * @param newLine NEWLINE node.
330      * @return true, if line is empty line.
331      */
332     private static boolean isEmptyLine(DetailNode newLine) {
333         boolean result = false;
334         DetailNode previousSibling = newLine.getPreviousSibling();
335         if (previousSibling != null && (previousSibling.getParent().getType()
336                 == JavadocCommentsTokenTypes.JAVADOC_CONTENT
337                 || insideNonTightHtml(previousSibling))) {
338             if (previousSibling.getType() == JavadocCommentsTokenTypes.TEXT
339                     && CommonUtil.isBlank(previousSibling.getText())) {
340                 previousSibling = previousSibling.getPreviousSibling();
341             }
342             result = previousSibling != null
343                     && previousSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK;
344         }
345         return result;
346     }
347 
348     /**
349      * Checks whether the given node is inside a non-tight HTML element.
350      *
351      * @param previousSibling the node to check
352      * @return true if inside non-tight HTML, false otherwise
353      */
354     private static boolean insideNonTightHtml(DetailNode previousSibling) {
355         final DetailNode parent = previousSibling.getParent();
356         DetailNode htmlElement = parent;
357         if (parent.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
358             htmlElement = parent.getParent();
359         }
360         return htmlElement.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT
361                 && JavadocUtil.findFirstToken(htmlElement,
362                     JavadocCommentsTokenTypes.HTML_TAG_END) == null;
363     }
364 
365     /**
366      * Determines whether or not the line with paragraph tag is first line in javadoc.
367      *
368      * @param paragraphTag paragraph tag.
369      * @return true, if line with paragraph tag is first line in javadoc.
370      */
371     private static boolean isFirstParagraph(DetailNode paragraphTag) {
372         boolean result = true;
373         DetailNode previousNode = paragraphTag.getPreviousSibling();
374         while (previousNode != null) {
375             if (previousNode.getType() == JavadocCommentsTokenTypes.TEXT
376                     && !CommonUtil.isBlank(previousNode.getText())
377                 || previousNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK
378                     && previousNode.getType() != JavadocCommentsTokenTypes.NEWLINE
379                     && previousNode.getType() != JavadocCommentsTokenTypes.TEXT) {
380                 result = false;
381                 break;
382             }
383             previousNode = previousNode.getPreviousSibling();
384         }
385         return result;
386     }
387 
388     /**
389      * Finds and returns nearest empty line in javadoc.
390      *
391      * @param node DetailNode node.
392      * @return Some nearest empty line in javadoc.
393      */
394     private static DetailNode getNearestEmptyLine(DetailNode node) {
395         DetailNode newLine = node;
396         while (newLine != null) {
397             final DetailNode previousSibling = newLine.getPreviousSibling();
398             if (newLine.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(newLine)) {
399                 break;
400             }
401             newLine = previousSibling;
402         }
403         return newLine;
404     }
405 
406     /**
407      * Tests whether the paragraph tag is immediately followed by the text.
408      *
409      * @param tag html tag.
410      * @return true, if the paragraph tag is immediately followed by the text.
411      */
412     private static boolean isImmediatelyFollowedByText(DetailNode tag) {
413         final DetailNode nextSibling = getNextSibling(tag);
414 
415         return nextSibling == null || nextSibling.getText().startsWith(" ");
416     }
417 
418     /**
419      * Tests whether the paragraph tag is immediately followed by the new line.
420      *
421      * @param tag html tag.
422      * @return true, if the paragraph tag is immediately followed by the new line.
423      */
424     private static boolean isImmediatelyFollowedByNewLine(DetailNode tag) {
425         final DetailNode sibling = getNextSibling(tag);
426         return sibling != null && sibling.getType() == JavadocCommentsTokenTypes.NEWLINE;
427     }
428 
429     /**
430      * Custom getNextSibling method to handle different types of paragraph tag.
431      * It works for both {@code <p>} and {@code <p></p>} tags.
432      *
433      * @param tag HTML_ELEMENT tag.
434      * @return next sibling of the tag.
435      */
436     private static DetailNode getNextSibling(DetailNode tag) {
437         DetailNode nextSibling;
438         final DetailNode paragraphStartTagToken = tag.getFirstChild();
439         final DetailNode nextNode = paragraphStartTagToken.getNextSibling();
440 
441         if (nextNode == null) {
442             nextSibling = tag.getNextSibling();
443         }
444         else if (nextNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
445             nextSibling = nextNode.getFirstChild();
446         }
447         else {
448             nextSibling = nextNode;
449         }
450 
451         if (nextSibling != null
452                 && nextSibling.getType() == JavadocCommentsTokenTypes.HTML_COMMENT) {
453             nextSibling = nextSibling.getNextSibling();
454         }
455         return nextSibling;
456     }
457 }