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      * Creates a new {@code JavadocParagraphCheck} instance.
119      */
120     public JavadocParagraphCheck() {
121         // no code by default
122     }
123 
124     /**
125      * Setter to control whether the {@literal <p>} tag should be placed
126      * immediately before the first word.
127      *
128      * @param value value to set.
129      * @since 6.9
130      */
131     public void setAllowNewlineParagraph(boolean value) {
132         allowNewlineParagraph = value;
133     }
134 
135     @Override
136     public int[] getDefaultJavadocTokens() {
137         return new int[] {
138             JavadocCommentsTokenTypes.NEWLINE,
139             JavadocCommentsTokenTypes.HTML_ELEMENT,
140         };
141     }
142 
143     @Override
144     public int[] getRequiredJavadocTokens() {
145         return getAcceptableJavadocTokens();
146     }
147 
148     @Override
149     public void visitJavadocToken(DetailNode ast) {
150         if (ast.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(ast)) {
151             checkEmptyLine(ast);
152         }
153         else if (JavadocUtil.isTag(ast, PARAGRAPH_TAG)) {
154             checkParagraphTag(ast);
155         }
156     }
157 
158     /**
159      * Determines whether or not the next line after empty line has paragraph tag in the beginning.
160      *
161      * @param newline NEWLINE node.
162      */
163     private void checkEmptyLine(DetailNode newline) {
164         final DetailNode nearestToken = getNearestNode(newline);
165         if (nearestToken != null && nearestToken.getType() == JavadocCommentsTokenTypes.TEXT
166                 && !CommonUtil.isBlank(nearestToken.getText())) {
167             log(newline, MSG_TAG_AFTER);
168         }
169     }
170 
171     /**
172      * Determines whether or not the line with paragraph tag has previous empty line.
173      *
174      * @param tag html tag.
175      */
176     private void checkParagraphTag(DetailNode tag) {
177         if (!isNestedParagraph(tag) && !isInsideBlockTag(tag)) {
178             final DetailNode newLine = getNearestEmptyLine(tag);
179             if (isFirstParagraph(tag)) {
180                 log(tag, MSG_REDUNDANT_PARAGRAPH);
181             }
182             else if (newLine == null || tag.getLineNumber() - newLine.getLineNumber() != 1) {
183                 log(tag, MSG_LINE_BEFORE);
184             }
185 
186             final String blockTagName = findFollowedBlockTagName(tag);
187             if (blockTagName != null) {
188                 log(tag, MSG_PRECEDED_BLOCK_TAG, blockTagName);
189             }
190 
191             if (!allowNewlineParagraph && isImmediatelyFollowedByNewLine(tag)) {
192                 log(tag, MSG_MISPLACED_TAG);
193             }
194             if (isImmediatelyFollowedByText(tag)) {
195                 log(tag, MSG_MISPLACED_TAG);
196             }
197         }
198     }
199 
200     /**
201      * Determines whether the paragraph tag is nested.
202      *
203      * @param tag html tag.
204      * @return true, if the paragraph tag is nested.
205      */
206     private static boolean isNestedParagraph(DetailNode tag) {
207         boolean nested = false;
208         DetailNode parent = tag.getParent();
209 
210         while (parent != null) {
211             if (parent.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
212                 nested = true;
213                 break;
214             }
215             parent = parent.getParent();
216         }
217 
218         return nested;
219     }
220 
221     /**
222      * Determines whether the paragraph tag is inside javadoc block tag.
223      *
224      * @param tag html tag.
225      * @return true, if the paragraph tag is inside javadoc block tag.
226      */
227     private static boolean isInsideBlockTag(DetailNode tag) {
228         boolean result = false;
229         DetailNode parent = tag;
230 
231         while (parent != null) {
232             if (parent.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
233                 result = true;
234                 break;
235             }
236             parent = parent.getParent();
237         }
238 
239         return result;
240     }
241 
242     /**
243      * Determines whether or not the paragraph tag is followed by block tag.
244      *
245      * @param tag html tag.
246      * @return block tag if the paragraph tag is followed by block tag or null if not found.
247      */
248     @Nullable
249     private static String findFollowedBlockTagName(DetailNode tag) {
250         final DetailNode htmlElement = findFirstHtmlElementAfter(tag);
251         String blockTagName = null;
252 
253         if (htmlElement != null) {
254             blockTagName = getHtmlElementName(htmlElement);
255         }
256 
257         return blockTagName;
258     }
259 
260     /**
261      * Finds and returns first html element after the tag.
262      *
263      * @param tag html tag.
264      * @return first html element after the paragraph tag or null if not found.
265      */
266     @Nullable
267     private static DetailNode findFirstHtmlElementAfter(DetailNode tag) {
268         DetailNode htmlElement = getNextSibling(tag);
269 
270         while (htmlElement != null
271                 && htmlElement.getType() != JavadocCommentsTokenTypes.HTML_ELEMENT) {
272             if (htmlElement.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
273                 htmlElement = htmlElement.getFirstChild();
274             }
275             else if (htmlElement.getType() == JavadocCommentsTokenTypes.TEXT
276                     && !CommonUtil.isBlank(htmlElement.getText())) {
277                 htmlElement = null;
278                 break;
279             }
280             else {
281                 htmlElement = htmlElement.getNextSibling();
282             }
283         }
284         if (htmlElement != null
285                 && JavadocUtil.findFirstToken(htmlElement,
286                         JavadocCommentsTokenTypes.HTML_TAG_END) == null) {
287             htmlElement = null;
288         }
289 
290         return htmlElement;
291     }
292 
293     /**
294      * Finds and returns first block-level html element name.
295      *
296      * @param htmlElement block-level html tag.
297      * @return block-level html element name or null if not found.
298      */
299     @Nullable
300     private static String getHtmlElementName(DetailNode htmlElement) {
301         final DetailNode htmlTagStart = htmlElement.getFirstChild();
302         final DetailNode htmlTagName =
303                 JavadocUtil.findFirstToken(htmlTagStart, JavadocCommentsTokenTypes.TAG_NAME);
304         String blockTagName = null;
305         if (BLOCK_TAGS.contains(htmlTagName.getText())) {
306             blockTagName = htmlTagName.getText();
307         }
308 
309         return blockTagName;
310     }
311 
312     /**
313      * Returns nearest node.
314      *
315      * @param node DetailNode node.
316      * @return nearest node.
317      */
318     private static DetailNode getNearestNode(DetailNode node) {
319         DetailNode currentNode = node;
320         while (currentNode != null
321                 && (currentNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
322                     || currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE)) {
323             currentNode = currentNode.getNextSibling();
324         }
325         if (currentNode != null
326                 && currentNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
327             currentNode = currentNode.getFirstChild();
328         }
329         return currentNode;
330     }
331 
332     /**
333      * Determines whether or not the line is empty line.
334      *
335      * @param newLine NEWLINE node.
336      * @return true, if line is empty line.
337      */
338     private static boolean isEmptyLine(DetailNode newLine) {
339         boolean result = false;
340         DetailNode previousSibling = newLine.getPreviousSibling();
341         if (previousSibling != null && (previousSibling.getParent().getType()
342                 == JavadocCommentsTokenTypes.JAVADOC_CONTENT
343                 || insideNonTightHtml(previousSibling))) {
344             if (previousSibling.getType() == JavadocCommentsTokenTypes.TEXT
345                     && CommonUtil.isBlank(previousSibling.getText())) {
346                 previousSibling = previousSibling.getPreviousSibling();
347             }
348             result = previousSibling != null
349                     && previousSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK;
350         }
351         return result;
352     }
353 
354     /**
355      * Checks whether the given node is inside a non-tight HTML element.
356      *
357      * @param previousSibling the node to check
358      * @return true if inside non-tight HTML, false otherwise
359      */
360     private static boolean insideNonTightHtml(DetailNode previousSibling) {
361         final DetailNode parent = previousSibling.getParent();
362         DetailNode htmlElement = parent;
363         if (parent.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
364             htmlElement = parent.getParent();
365         }
366         return htmlElement.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT
367                 && JavadocUtil.findFirstToken(htmlElement,
368                     JavadocCommentsTokenTypes.HTML_TAG_END) == null;
369     }
370 
371     /**
372      * Determines whether or not the line with paragraph tag is first line in javadoc.
373      *
374      * @param paragraphTag paragraph tag.
375      * @return true, if line with paragraph tag is first line in javadoc.
376      */
377     private static boolean isFirstParagraph(DetailNode paragraphTag) {
378         boolean result = true;
379         DetailNode previousNode = paragraphTag.getPreviousSibling();
380         while (previousNode != null) {
381             if (previousNode.getType() == JavadocCommentsTokenTypes.TEXT
382                     && !CommonUtil.isBlank(previousNode.getText())
383                 || previousNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK
384                     && previousNode.getType() != JavadocCommentsTokenTypes.NEWLINE
385                     && previousNode.getType() != JavadocCommentsTokenTypes.TEXT) {
386                 result = false;
387                 break;
388             }
389             previousNode = previousNode.getPreviousSibling();
390         }
391         return result;
392     }
393 
394     /**
395      * Finds and returns nearest empty line in javadoc.
396      *
397      * @param node DetailNode node.
398      * @return Some nearest empty line in javadoc.
399      */
400     private static DetailNode getNearestEmptyLine(DetailNode node) {
401         DetailNode newLine = node;
402         while (newLine != null) {
403             final DetailNode previousSibling = newLine.getPreviousSibling();
404             if (newLine.getType() == JavadocCommentsTokenTypes.NEWLINE && isEmptyLine(newLine)) {
405                 break;
406             }
407             newLine = previousSibling;
408         }
409         return newLine;
410     }
411 
412     /**
413      * Tests whether the paragraph tag is immediately followed by the text.
414      *
415      * @param tag html tag.
416      * @return true, if the paragraph tag is immediately followed by the text.
417      */
418     private static boolean isImmediatelyFollowedByText(DetailNode tag) {
419         final DetailNode nextSibling = getNextSibling(tag);
420 
421         return nextSibling == null || nextSibling.getText().startsWith(" ");
422     }
423 
424     /**
425      * Tests whether the paragraph tag is immediately followed by the new line.
426      *
427      * @param tag html tag.
428      * @return true, if the paragraph tag is immediately followed by the new line.
429      */
430     private static boolean isImmediatelyFollowedByNewLine(DetailNode tag) {
431         final DetailNode sibling = getNextSibling(tag);
432         return sibling != null && sibling.getType() == JavadocCommentsTokenTypes.NEWLINE;
433     }
434 
435     /**
436      * Custom getNextSibling method to handle different types of paragraph tag.
437      * It works for both {@code <p>} and {@code <p></p>} tags.
438      *
439      * @param tag HTML_ELEMENT tag.
440      * @return next sibling of the tag.
441      */
442     private static DetailNode getNextSibling(DetailNode tag) {
443         DetailNode nextSibling;
444         final DetailNode paragraphStartTagToken = tag.getFirstChild();
445         final DetailNode nextNode = paragraphStartTagToken.getNextSibling();
446 
447         if (nextNode == null) {
448             nextSibling = tag.getNextSibling();
449         }
450         else if (nextNode.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
451             nextSibling = nextNode.getFirstChild();
452         }
453         else {
454             nextSibling = nextNode;
455         }
456 
457         if (nextSibling != null
458                 && nextSibling.getType() == JavadocCommentsTokenTypes.HTML_COMMENT) {
459             nextSibling = nextSibling.getNextSibling();
460         }
461         return nextSibling;
462     }
463 
464 }