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