001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.javadoc;
021
022import java.util.Set;
023
024import javax.annotation.Nullable;
025
026import com.puppycrawl.tools.checkstyle.StatelessCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailNode;
028import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
029import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
030import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
031
032/**
033 * <div>
034 * Checks the Javadoc paragraph.
035 * </div>
036 *
037 * <p>
038 * Checks that:
039 * </p>
040 * <ul>
041 * <li>There is one blank line between each of two paragraphs.</li>
042 * <li>Each paragraph but the first has {@literal <p>} immediately
043 * before the first word, with no space after.</li>
044 * <li>The outer most paragraph tags should not precede
045 * <a href="https://www.w3schools.com/html/html_blocks.asp">HTML block-tag</a>.
046 * Nested paragraph tags are allowed to do that. This check only supports following block-tags:
047 * {@literal <address>},{@literal <blockquote>}
048 * ,{@literal <div>},{@literal <dl>}
049 * ,{@literal <h1>},{@literal <h2>},{@literal <h3>},{@literal <h4>},{@literal <h5>},
050 * {@literal <h6>},{@literal <hr>}
051 * ,{@literal <ol>},{@literal <p>},{@literal <pre>}
052 * ,{@literal <table>},{@literal <ul>}.
053 * </li>
054 * </ul>
055 *
056 * <p><b>ATTENTION:</b></p>
057 *
058 * <p>This Check ignores HTML comments.</p>
059 *
060 * <p>The Check ignores all the nested paragraph tags,
061 * it will not give any kind of violation if the paragraph tag is nested.
062 * It also ignores paragraph tags inside block tags.</p>
063 *
064 * @since 6.0
065 */
066@StatelessCheck
067public class JavadocParagraphCheck extends AbstractJavadocCheck {
068
069    /**
070     * A key is pointing to the warning message text in "messages.properties"
071     * file.
072     */
073    public static final String MSG_TAG_AFTER = "javadoc.paragraph.tag.after";
074
075    /**
076     * A key is pointing to the warning message text in "messages.properties"
077     * file.
078     */
079    public static final String MSG_LINE_BEFORE = "javadoc.paragraph.line.before";
080
081    /**
082     * A key is pointing to the warning message text in "messages.properties"
083     * file.
084     */
085    public static final String MSG_REDUNDANT_PARAGRAPH = "javadoc.paragraph.redundant.paragraph";
086
087    /**
088     * A key is pointing to the warning message text in "messages.properties"
089     * file.
090     */
091    public static final String MSG_MISPLACED_TAG = "javadoc.paragraph.misplaced.tag";
092
093    /**
094     * A key is pointing to the warning message text in "messages.properties"
095     * file.
096     */
097    public static final String MSG_PRECEDED_BLOCK_TAG = "javadoc.paragraph.preceded.block.tag";
098
099    /**
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}