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