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