View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.List;
25  
26  import com.puppycrawl.tools.checkstyle.StatelessCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailNode;
28  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
31  
32  /**
33   * <div>
34   * Checks that one blank line before the block tag if it is present in Javadoc.
35   * </div>
36   *
37   * @since 8.36
38   */
39  @StatelessCheck
40  @SuppressWarnings("InvalidInlineTag")
41  public class RequireEmptyLineBeforeBlockTagGroupCheck extends AbstractJavadocCheck {
42  
43      /**
44       * The key in "messages.properties" for the message that describes a tag in javadoc
45       * requiring an empty line before it.
46       */
47      public static final String MSG_JAVADOC_TAG_LINE_BEFORE = "javadoc.tag.line.before";
48  
49      /**
50       * Case when space separates the tag and the asterisk like in the below example.
51       * <pre>
52       *  /**
53       *   * &#64;param noSpace there is no space here
54       * </pre>
55       */
56      private static final List<Integer> ONLY_TAG_VARIATION_1 = Arrays.asList(
57              JavadocCommentsTokenTypes.TEXT,
58              JavadocCommentsTokenTypes.LEADING_ASTERISK,
59              JavadocCommentsTokenTypes.NEWLINE);
60  
61      /**
62       * Case when no space separates the tag and the asterisk like in the below example.
63       * <pre>
64       *  /**
65       *   *&#64;param noSpace there is no space here
66       * </pre>
67       */
68      private static final List<Integer> ONLY_TAG_VARIATION_2 = Arrays.asList(
69              JavadocCommentsTokenTypes.LEADING_ASTERISK,
70              JavadocCommentsTokenTypes.NEWLINE);
71  
72      /**
73       * Case when space separates the tag and multiple asterisks like in the below example.
74       * {@snippet lang="text" :
75       *  /**
76       *   * @param noSpace there is no space here
77       * }
78       */
79      private static final List<Integer> ONLY_TAG_VARIATION_3 = Arrays.asList(
80              JavadocCommentsTokenTypes.TEXT,
81              JavadocCommentsTokenTypes.LEADING_ASTERISKS,
82              JavadocCommentsTokenTypes.NEWLINE);
83  
84      /**
85       * Case when no space separates the tag and multiple asterisks like in the below example.
86       * {@snippet lang="text" :
87       *  /**
88       *   **@param noSpace there is no space here
89       * }
90       */
91      private static final List<Integer> ONLY_TAG_VARIATION_4 = Arrays.asList(
92              JavadocCommentsTokenTypes.LEADING_ASTERISKS,
93              JavadocCommentsTokenTypes.NEWLINE);
94  
95      /**
96       * Case when one extra asterisk is in the javadoc start like in the below example.
97       * {@snippet lang="text" :
98       *  /***
99       *   * @param noSpace there is no space here
100      * }
101      */
102     private static final List<Integer> ONLY_TAG_VARIATION_5 = Arrays.asList(
103             JavadocCommentsTokenTypes.TEXT,
104             JavadocCommentsTokenTypes.LEADING_ASTERISK,
105             JavadocCommentsTokenTypes.NEWLINE,
106             JavadocCommentsTokenTypes.LEADING_ASTERISK);
107 
108     /**
109      * Case when a boxed javadoc has only a block tag like in the below example.
110      * {@snippet lang="text" :
111      *  /*********
112      *   *** @param noSpace there is no space here
113      * }
114      */
115     private static final List<Integer> ONLY_TAG_VARIATION_6 = Arrays.asList(
116             JavadocCommentsTokenTypes.TEXT,
117             JavadocCommentsTokenTypes.LEADING_ASTERISKS,
118             JavadocCommentsTokenTypes.NEWLINE,
119             JavadocCommentsTokenTypes.LEADING_ASTERISKS);
120 
121     /**
122      * Creates a new {@code RequireEmptyLineBeforeBlockTagGroupCheck} instance.
123      */
124     public RequireEmptyLineBeforeBlockTagGroupCheck() {
125         // no code by default
126     }
127 
128     /**
129      * Returns only javadoc tags so visitJavadocToken only receives javadoc tags.
130      *
131      * @return only javadoc tags.
132      */
133     @Override
134     public int[] getDefaultJavadocTokens() {
135         return new int[] {
136             JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG,
137         };
138     }
139 
140     @Override
141     public int[] getRequiredJavadocTokens() {
142         return getAcceptableJavadocTokens();
143     }
144 
145     /**
146      * Logs when there is no empty line before the tag.
147      *
148      * @param tagNode the at tag node to check for an empty space before it.
149      */
150     @Override
151     public void visitJavadocToken(DetailNode tagNode) {
152         // No need to filter token because overridden getDefaultJavadocTokens ensures that we only
153         // receive JAVADOC_BLOCK_TAG DetailNode.
154         if (!isAnotherTagBefore(tagNode)
155                 && !isOnlyTagInWholeJavadoc(tagNode)
156                 && hasInsufficientConsecutiveNewlines(tagNode)) {
157             final String tagName = JavadocUtil.getTagName(tagNode);
158             log(tagNode.getLineNumber(),
159                     MSG_JAVADOC_TAG_LINE_BEFORE,
160                     "@" + tagName);
161         }
162     }
163 
164     /**
165      * Returns true when there is a javadoc tag before the provided tagNode.
166      *
167      * @param tagNode the javadoc tag node, to look for more tags before it.
168      * @return true when there is a javadoc tag before the provided tagNode.
169      */
170     private static boolean isAnotherTagBefore(DetailNode tagNode) {
171         boolean found = false;
172         DetailNode currentNode = tagNode.getPreviousSibling();
173         while (currentNode != null) {
174             if (currentNode.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
175                 found = true;
176                 break;
177             }
178             currentNode = currentNode.getPreviousSibling();
179         }
180         return found;
181     }
182 
183     /**
184      * Returns true when there are is only whitespace and asterisks before the provided tagNode.
185      * When javadoc has only a javadoc tag like {@literal @} in it, the JAVADOC_TAG in a JAVADOC
186      * detail node will always have 2 or 3 siblings before it. The parse tree looks like:
187      * {@snippet lang="text" :
188      * JAVADOC_CONTENT[3x0]
189      * |--NEWLINE[3x0] : [\n]
190      * |--LEADING_ASTERISK[4x1] : [*]
191      * |--TEXT[4x2] : [ ]
192      * |--JAVADOC_BLOCK_TAG[4x3] : [@param T The bar.\n ]
193      * }
194      * Or it can also look like:
195      * {@snippet lang="text" :
196      * JAVADOC_CONTENT[3x0]
197      * |--NEWLINE[3x0] : [\n]
198      * |--LEADING_ASTERISK[4x1] : [*]
199      * |--JAVADOC_BLOCK_TAG[4x3] : [@param T The bar.\n ]
200      * }
201      * We do not include the variation
202      * {@snippet lang="text" :
203      *  /**@param noSpace there is no space here
204      * }
205      * which results in the tree
206      * {@snippet lang="text" :
207      * JAVADOC_CONTENT[3x0]
208      * |--JAVADOC_BLOCK_TAG[4x3] : [@param noSpace there is no space here\n ]
209      * }
210      * because this one is invalid. We must recommend placing a blank line to separate &#64;param
211      * from the first javadoc asterisks.
212      *
213      * @param tagNode the at tag node to check if there is nothing before it
214      * @return true if there is no text before the tagNode
215      */
216     @SuppressWarnings("InvalidInlineTag")
217     private static boolean isOnlyTagInWholeJavadoc(DetailNode tagNode) {
218         final List<Integer> previousNodeTypes = new ArrayList<>();
219         DetailNode currentNode = tagNode.getPreviousSibling();
220 
221         while (currentNode != null) {
222             previousNodeTypes.add(currentNode.getType());
223             currentNode = currentNode.getPreviousSibling();
224         }
225         return ONLY_TAG_VARIATION_1.equals(previousNodeTypes)
226                 || ONLY_TAG_VARIATION_2.equals(previousNodeTypes)
227                 || ONLY_TAG_VARIATION_3.equals(previousNodeTypes)
228                 || ONLY_TAG_VARIATION_4.equals(previousNodeTypes)
229                 || ONLY_TAG_VARIATION_5.equals(previousNodeTypes)
230                 || ONLY_TAG_VARIATION_6.equals(previousNodeTypes);
231     }
232 
233     /**
234      * Returns true when there are not enough empty lines before the provided tagNode.
235      *
236      * <p>Iterates through the previous siblings of the tagNode looking for empty lines until
237      * there are no more siblings or it hits something other than asterisk, whitespace or newline.
238      * If it finds at least one empty line, return true. Return false otherwise.</p>
239      *
240      * @param tagNode the tagNode to check if there are sufficient empty lines before it.
241      * @return true if there are not enough empty lines before the tagNode.
242      */
243     private static boolean hasInsufficientConsecutiveNewlines(DetailNode tagNode) {
244         int count = 0;
245         DetailNode currentNode = tagNode.getPreviousSibling();
246         while (currentNode != null
247                 && (CommonUtil.isBlank(currentNode.getText())
248                 || isLeadingAsterisk(currentNode))) {
249             if (currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE) {
250                 count++;
251             }
252             currentNode = currentNode.getPreviousSibling();
253         }
254 
255         return count <= 1;
256     }
257 
258     /**
259      * Checks if current node is leading asterisk.
260      *
261      * @param detailNode the node to check.
262      * @return true when token is a leading asterisk.
263      */
264     private static boolean isLeadingAsterisk(DetailNode detailNode) {
265         return detailNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
266                 || detailNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISKS;
267     }
268 
269 }