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