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 * @since 8.36
037 */
038@StatelessCheck
039public class RequireEmptyLineBeforeBlockTagGroupCheck extends AbstractJavadocCheck {
040
041    /**
042     * The key in "messages.properties" for the message that describes a tag in javadoc
043     * requiring an empty line before it.
044     */
045    public static final String MSG_JAVADOC_TAG_LINE_BEFORE = "javadoc.tag.line.before";
046
047    /**
048     * Case when space separates the tag and the asterisk like in the below example.
049     * <pre>
050     *  /**
051     *   * &#64;param noSpace there is no space here
052     * </pre>
053     */
054    private static final List<Integer> ONLY_TAG_VARIATION_1 = Arrays.asList(
055            JavadocTokenTypes.WS,
056            JavadocTokenTypes.LEADING_ASTERISK,
057            JavadocTokenTypes.NEWLINE);
058
059    /**
060     * Case when no space separates the tag and the asterisk like in the below example.
061     * <pre>
062     *  /**
063     *   *&#64;param noSpace there is no space here
064     * </pre>
065     */
066    private static final List<Integer> ONLY_TAG_VARIATION_2 = Arrays.asList(
067            JavadocTokenTypes.LEADING_ASTERISK,
068            JavadocTokenTypes.NEWLINE);
069
070    /**
071     * Returns only javadoc tags so visitJavadocToken only receives javadoc tags.
072     *
073     * @return only javadoc tags.
074     */
075    @Override
076    public int[] getDefaultJavadocTokens() {
077        return new int[] {
078            JavadocTokenTypes.JAVADOC_TAG,
079        };
080    }
081
082    @Override
083    public int[] getRequiredJavadocTokens() {
084        return getAcceptableJavadocTokens();
085    }
086
087    /**
088     * Logs when there is no empty line before the tag.
089     *
090     * @param tagNode the at tag node to check for an empty space before it.
091     */
092    @Override
093    public void visitJavadocToken(DetailNode tagNode) {
094        // No need to filter token because overridden getDefaultJavadocTokens ensures that we only
095        // receive JAVADOC_TAG DetailNode.
096        if (!isAnotherTagBefore(tagNode)
097                && !isOnlyTagInWholeJavadoc(tagNode)
098                && hasInsufficientConsecutiveNewlines(tagNode)) {
099            log(tagNode.getLineNumber(),
100                    MSG_JAVADOC_TAG_LINE_BEFORE,
101                    tagNode.getChildren()[0].getText());
102        }
103    }
104
105    /**
106     * Returns true when there is a javadoc tag before the provided tagNode.
107     *
108     * @param tagNode the javadoc tag node, to look for more tags before it.
109     * @return true when there is a javadoc tag before the provided tagNode.
110     */
111    private static boolean isAnotherTagBefore(DetailNode tagNode) {
112        boolean found = false;
113        DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
114        while (currentNode != null) {
115            if (currentNode.getType() == JavadocTokenTypes.JAVADOC_TAG) {
116                found = true;
117                break;
118            }
119            currentNode = JavadocUtil.getPreviousSibling(currentNode);
120        }
121        return found;
122    }
123
124    /**
125     * Returns true when there are is only whitespace and asterisks before the provided tagNode.
126     * When javadoc has only a javadoc tag like {@literal @} in it, the JAVADOC_TAG in a JAVADOC
127     * detail node will always have 2 or 3 siblings before it. The parse tree looks like:
128     * <pre>
129     * JAVADOC[3x0]
130     * |--NEWLINE[3x0] : [\n]
131     * |--LEADING_ASTERISK[4x0] : [ *]
132     * |--WS[4x2] : [ ]
133     * |--JAVADOC_TAG[4x3] : [@param T The bar.\n ]
134     * </pre>
135     * Or it can also look like:
136     * <pre>
137     * JAVADOC[3x0]
138     * |--NEWLINE[3x0] : [\n]
139     * |--LEADING_ASTERISK[4x0] : [ *]
140     * |--JAVADOC_TAG[4x3] : [@param T The bar.\n ]
141     * </pre>
142     * We do not include the variation
143     * <pre>
144     *  /**&#64;param noSpace there is no space here
145     * </pre>
146     * which results in the tree
147     * <pre>
148     * JAVADOC[3x0]
149     * |--JAVADOC_TAG[4x3] : [@param noSpace there is no space here\n ]
150     * </pre>
151     * because this one is invalid. We must recommend placing a blank line to separate &#64;param
152     * from the first javadoc asterisks.
153     *
154     * @param tagNode the at tag node to check if there is nothing before it
155     * @return true if there is no text before the tagNode
156     */
157    private static boolean isOnlyTagInWholeJavadoc(DetailNode tagNode) {
158        final List<Integer> previousNodeTypes = new ArrayList<>();
159        DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
160        while (currentNode != null) {
161            previousNodeTypes.add(currentNode.getType());
162            currentNode = JavadocUtil.getPreviousSibling(currentNode);
163        }
164        return ONLY_TAG_VARIATION_1.equals(previousNodeTypes)
165                || ONLY_TAG_VARIATION_2.equals(previousNodeTypes);
166    }
167
168    /**
169     * Returns true when there are not enough empty lines before the provided tagNode.
170     *
171     * <p>Iterates through the previous siblings of the tagNode looking for empty lines until
172     * there are no more siblings or it hits something other than asterisk, whitespace or newline.
173     * If it finds at least one empty line, return true. Return false otherwise.</p>
174     *
175     * @param tagNode the tagNode to check if there are sufficient empty lines before it.
176     * @return true if there are not enough empty lines before the tagNode.
177     */
178    private static boolean hasInsufficientConsecutiveNewlines(DetailNode tagNode) {
179        int count = 0;
180        DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
181        while (currentNode != null
182                && (currentNode.getType() == JavadocTokenTypes.NEWLINE
183                || currentNode.getType() == JavadocTokenTypes.WS
184                || currentNode.getType() == JavadocTokenTypes.LEADING_ASTERISK)) {
185            if (currentNode.getType() == JavadocTokenTypes.NEWLINE) {
186                count++;
187            }
188            currentNode = JavadocUtil.getPreviousSibling(currentNode);
189        }
190
191        return count <= 1;
192    }
193}