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 * * @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 * *@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 * /**@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 @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 }