001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.ArrayList;
023import java.util.Arrays;
024import java.util.List;
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.JavadocUtil;
030
031/**
032 * <div>
033 * Checks that one blank line before the block tag if it is present in Javadoc.
034 * </div>
035 *
036 * <ul>
037 * <li>
038 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations
039 * if the Javadoc being examined by this check violates the tight html rules defined at
040 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
041 * Tight-HTML Rules</a>.
042 * Type is {@code boolean}.
043 * Default value is {@code false}.
044 * </li>
045 * </ul>
046 *
047 * <p>
048 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
049 * </p>
050 *
051 * <p>
052 * Violation Message Keys:
053 * </p>
054 * <ul>
055 * <li>
056 * {@code javadoc.missed.html.close}
057 * </li>
058 * <li>
059 * {@code javadoc.parse.rule.error}
060 * </li>
061 * <li>
062 * {@code javadoc.tag.line.before}
063 * </li>
064 * <li>
065 * {@code javadoc.unclosedHtml}
066 * </li>
067 * <li>
068 * {@code javadoc.wrong.singleton.html.tag}
069 * </li>
070 * </ul>
071 *
072 * @since 8.36
073 */
074@StatelessCheck
075public class RequireEmptyLineBeforeBlockTagGroupCheck extends AbstractJavadocCheck {
076
077    /**
078     * The key in "messages.properties" for the message that describes a tag in javadoc
079     * requiring an empty line before it.
080     */
081    public static final String MSG_JAVADOC_TAG_LINE_BEFORE = "javadoc.tag.line.before";
082
083    /**
084     * Case when space separates the tag and the asterisk like in the below example.
085     * <pre>
086     *  /**
087     *   * &#64;param noSpace there is no space here
088     * </pre>
089     */
090    private static final List<Integer> ONLY_TAG_VARIATION_1 = Arrays.asList(
091            JavadocTokenTypes.WS,
092            JavadocTokenTypes.LEADING_ASTERISK,
093            JavadocTokenTypes.NEWLINE);
094
095    /**
096     * Case when no space separates the tag and the asterisk like in the below example.
097     * <pre>
098     *  /**
099     *   *&#64;param noSpace there is no space here
100     * </pre>
101     */
102    private static final List<Integer> ONLY_TAG_VARIATION_2 = Arrays.asList(
103            JavadocTokenTypes.LEADING_ASTERISK,
104            JavadocTokenTypes.NEWLINE);
105
106    /**
107     * Returns only javadoc tags so visitJavadocToken only receives javadoc tags.
108     *
109     * @return only javadoc tags.
110     */
111    @Override
112    public int[] getDefaultJavadocTokens() {
113        return new int[] {
114            JavadocTokenTypes.JAVADOC_TAG,
115        };
116    }
117
118    @Override
119    public int[] getRequiredJavadocTokens() {
120        return getAcceptableJavadocTokens();
121    }
122
123    /**
124     * Logs when there is no empty line before the tag.
125     *
126     * @param tagNode the at tag node to check for an empty space before it.
127     */
128    @Override
129    public void visitJavadocToken(DetailNode tagNode) {
130        // No need to filter token because overridden getDefaultJavadocTokens ensures that we only
131        // receive JAVADOC_TAG DetailNode.
132        if (!isAnotherTagBefore(tagNode)
133                && !isOnlyTagInWholeJavadoc(tagNode)
134                && hasInsufficientConsecutiveNewlines(tagNode)) {
135            log(tagNode.getLineNumber(),
136                    MSG_JAVADOC_TAG_LINE_BEFORE,
137                    tagNode.getChildren()[0].getText());
138        }
139    }
140
141    /**
142     * Returns true when there is a javadoc tag before the provided tagNode.
143     *
144     * @param tagNode the javadoc tag node, to look for more tags before it.
145     * @return true when there is a javadoc tag before the provided tagNode.
146     */
147    private static boolean isAnotherTagBefore(DetailNode tagNode) {
148        boolean found = false;
149        DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
150        while (currentNode != null) {
151            if (currentNode.getType() == JavadocTokenTypes.JAVADOC_TAG) {
152                found = true;
153                break;
154            }
155            currentNode = JavadocUtil.getPreviousSibling(currentNode);
156        }
157        return found;
158    }
159
160    /**
161     * Returns true when there are is only whitespace and asterisks before the provided tagNode.
162     * When javadoc has only a javadoc tag like {@literal @} in it, the JAVADOC_TAG in a JAVADOC
163     * detail node will always have 2 or 3 siblings before it. The parse tree looks like:
164     * <pre>
165     * JAVADOC[3x0]
166     * |--NEWLINE[3x0] : [\n]
167     * |--LEADING_ASTERISK[4x0] : [ *]
168     * |--WS[4x2] : [ ]
169     * |--JAVADOC_TAG[4x3] : [@param T The bar.\n ]
170     * </pre>
171     * Or it can also look like:
172     * <pre>
173     * JAVADOC[3x0]
174     * |--NEWLINE[3x0] : [\n]
175     * |--LEADING_ASTERISK[4x0] : [ *]
176     * |--JAVADOC_TAG[4x3] : [@param T The bar.\n ]
177     * </pre>
178     * We do not include the variation
179     * <pre>
180     *  /**&#64;param noSpace there is no space here
181     * </pre>
182     * which results in the tree
183     * <pre>
184     * JAVADOC[3x0]
185     * |--JAVADOC_TAG[4x3] : [@param noSpace there is no space here\n ]
186     * </pre>
187     * because this one is invalid. We must recommend placing a blank line to separate &#64;param
188     * from the first javadoc asterisks.
189     *
190     * @param tagNode the at tag node to check if there is nothing before it
191     * @return true if there is no text before the tagNode
192     */
193    private static boolean isOnlyTagInWholeJavadoc(DetailNode tagNode) {
194        final List<Integer> previousNodeTypes = new ArrayList<>();
195        DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
196        while (currentNode != null) {
197            previousNodeTypes.add(currentNode.getType());
198            currentNode = JavadocUtil.getPreviousSibling(currentNode);
199        }
200        return ONLY_TAG_VARIATION_1.equals(previousNodeTypes)
201                || ONLY_TAG_VARIATION_2.equals(previousNodeTypes);
202    }
203
204    /**
205     * Returns true when there are not enough empty lines before the provided tagNode.
206     *
207     * <p>Iterates through the previous siblings of the tagNode looking for empty lines until
208     * there are no more siblings or it hits something other than asterisk, whitespace or newline.
209     * If it finds at least one empty line, return true. Return false otherwise.</p>
210     *
211     * @param tagNode the tagNode to check if there are sufficient empty lines before it.
212     * @return true if there are not enough empty lines before the tagNode.
213     */
214    private static boolean hasInsufficientConsecutiveNewlines(DetailNode tagNode) {
215        int count = 0;
216        DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
217        while (currentNode != null
218                && (currentNode.getType() == JavadocTokenTypes.NEWLINE
219                || currentNode.getType() == JavadocTokenTypes.WS
220                || currentNode.getType() == JavadocTokenTypes.LEADING_ASTERISK)) {
221            if (currentNode.getType() == JavadocTokenTypes.NEWLINE) {
222                count++;
223            }
224            currentNode = JavadocUtil.getPreviousSibling(currentNode);
225        }
226
227        return count <= 1;
228    }
229}