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