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