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  public class RequireEmptyLineBeforeBlockTagGroupCheck extends AbstractJavadocCheck {
41  
42      /**
43       * The key in "messages.properties" for the message that describes a tag in javadoc
44       * requiring an empty line before it.
45       */
46      public static final String MSG_JAVADOC_TAG_LINE_BEFORE = "javadoc.tag.line.before";
47  
48      /**
49       * Case when space separates the tag and the asterisk like in the below example.
50       * <pre>
51       *  /**
52       *   * &#64;param noSpace there is no space here
53       * </pre>
54       */
55      private static final List<Integer> ONLY_TAG_VARIATION_1 = Arrays.asList(
56              JavadocCommentsTokenTypes.TEXT,
57              JavadocCommentsTokenTypes.LEADING_ASTERISK,
58              JavadocCommentsTokenTypes.NEWLINE);
59  
60      /**
61       * Case when no space separates the tag and the asterisk like in the below example.
62       * <pre>
63       *  /**
64       *   *&#64;param noSpace there is no space here
65       * </pre>
66       */
67      private static final List<Integer> ONLY_TAG_VARIATION_2 = Arrays.asList(
68              JavadocCommentsTokenTypes.LEADING_ASTERISK,
69              JavadocCommentsTokenTypes.NEWLINE);
70  
71      /**
72       * Returns only javadoc tags so visitJavadocToken only receives javadoc tags.
73       *
74       * @return only javadoc tags.
75       */
76      @Override
77      public int[] getDefaultJavadocTokens() {
78          return new int[] {
79              JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG,
80          };
81      }
82  
83      @Override
84      public int[] getRequiredJavadocTokens() {
85          return getAcceptableJavadocTokens();
86      }
87  
88      /**
89       * Logs when there is no empty line before the tag.
90       *
91       * @param tagNode the at tag node to check for an empty space before it.
92       */
93      @Override
94      public void visitJavadocToken(DetailNode tagNode) {
95          // No need to filter token because overridden getDefaultJavadocTokens ensures that we only
96          // receive JAVADOC_BLOCK_TAG DetailNode.
97          if (!isAnotherTagBefore(tagNode)
98                  && !isOnlyTagInWholeJavadoc(tagNode)
99                  && 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 }