001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.JavadocTokenTypes;
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 &lt;p&gt; 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 * &lt;address&gt;,&lt;blockquote&gt;
048 * ,&lt;div&gt;,&lt;dl&gt;
049 * ,&lt;h1&gt;,&lt;h2&gt;,&lt;h3&gt;,&lt;h4&gt;,&lt;h5&gt;,&lt;h6&gt;,&lt;hr&gt;
050 * ,&lt;ol&gt;,&lt;p&gt;,&lt;pre&gt;
051 * ,&lt;table&gt;,&lt;ul&gt;.
052 * </li>
053 * </ul>
054 *
055 * <p><b>ATTENTION:</b></p>
056 *
057 * <p>This Check ignores HTML comments.</p>
058 *
059 * <p>The Check ignores all the nested paragraph tags,
060 * it will not give any kind of violation if the paragraph tag is nested.</p>
061 * <ul>
062 * <li>
063 * Property {@code allowNewlineParagraph} - Control whether the &lt;p&gt; tag
064 * should be placed immediately before the first word.
065 * Type is {@code boolean}.
066 * Default value is {@code true}.
067 * </li>
068 * <li>
069 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations
070 * if the Javadoc being examined by this check violates the tight html rules defined at
071 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
072 * Tight-HTML Rules</a>.
073 * Type is {@code boolean}.
074 * Default value is {@code false}.
075 * </li>
076 * </ul>
077 *
078 * <p>
079 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
080 * </p>
081 *
082 * <p>
083 * Violation Message Keys:
084 * </p>
085 * <ul>
086 * <li>
087 * {@code javadoc.missed.html.close}
088 * </li>
089 * <li>
090 * {@code javadoc.paragraph.line.before}
091 * </li>
092 * <li>
093 * {@code javadoc.paragraph.misplaced.tag}
094 * </li>
095 * <li>
096 * {@code javadoc.paragraph.preceded.block.tag}
097 * </li>
098 * <li>
099 * {@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
118public 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}