1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2024 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.JavadocTokenTypes;
29 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
30
31 /**
32 * <div>
33 * Checks that one blank line before the block tag if it is present in Javadoc.
34 * </div>
35 *
36 * <ul>
37 * <li>
38 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations
39 * if the Javadoc being examined by this check violates the tight html rules defined at
40 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
41 * Tight-HTML Rules</a>.
42 * Type is {@code boolean}.
43 * Default value is {@code false}.
44 * </li>
45 * </ul>
46 *
47 * <p>
48 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
49 * </p>
50 *
51 * <p>
52 * Violation Message Keys:
53 * </p>
54 * <ul>
55 * <li>
56 * {@code javadoc.missed.html.close}
57 * </li>
58 * <li>
59 * {@code javadoc.parse.rule.error}
60 * </li>
61 * <li>
62 * {@code javadoc.tag.line.before}
63 * </li>
64 * <li>
65 * {@code javadoc.unclosedHtml}
66 * </li>
67 * <li>
68 * {@code javadoc.wrong.singleton.html.tag}
69 * </li>
70 * </ul>
71 *
72 * @since 8.36
73 */
74 @StatelessCheck
75 public class RequireEmptyLineBeforeBlockTagGroupCheck extends AbstractJavadocCheck {
76
77 /**
78 * The key in "messages.properties" for the message that describes a tag in javadoc
79 * requiring an empty line before it.
80 */
81 public static final String MSG_JAVADOC_TAG_LINE_BEFORE = "javadoc.tag.line.before";
82
83 /**
84 * Case when space separates the tag and the asterisk like in the below example.
85 * <pre>
86 * /**
87 * * @param noSpace there is no space here
88 * </pre>
89 */
90 private static final List<Integer> ONLY_TAG_VARIATION_1 = Arrays.asList(
91 JavadocTokenTypes.WS,
92 JavadocTokenTypes.LEADING_ASTERISK,
93 JavadocTokenTypes.NEWLINE);
94
95 /**
96 * Case when no space separates the tag and the asterisk like in the below example.
97 * <pre>
98 * /**
99 * *@param noSpace there is no space here
100 * </pre>
101 */
102 private static final List<Integer> ONLY_TAG_VARIATION_2 = Arrays.asList(
103 JavadocTokenTypes.LEADING_ASTERISK,
104 JavadocTokenTypes.NEWLINE);
105
106 /**
107 * Returns only javadoc tags so visitJavadocToken only receives javadoc tags.
108 *
109 * @return only javadoc tags.
110 */
111 @Override
112 public int[] getDefaultJavadocTokens() {
113 return new int[] {
114 JavadocTokenTypes.JAVADOC_TAG,
115 };
116 }
117
118 @Override
119 public int[] getRequiredJavadocTokens() {
120 return getAcceptableJavadocTokens();
121 }
122
123 /**
124 * Logs when there is no empty line before the tag.
125 *
126 * @param tagNode the at tag node to check for an empty space before it.
127 */
128 @Override
129 public void visitJavadocToken(DetailNode tagNode) {
130 // No need to filter token because overridden getDefaultJavadocTokens ensures that we only
131 // receive JAVADOC_TAG DetailNode.
132 if (!isAnotherTagBefore(tagNode)
133 && !isOnlyTagInWholeJavadoc(tagNode)
134 && hasInsufficientConsecutiveNewlines(tagNode)) {
135 log(tagNode.getLineNumber(),
136 MSG_JAVADOC_TAG_LINE_BEFORE,
137 tagNode.getChildren()[0].getText());
138 }
139 }
140
141 /**
142 * Returns true when there is a javadoc tag before the provided tagNode.
143 *
144 * @param tagNode the javadoc tag node, to look for more tags before it.
145 * @return true when there is a javadoc tag before the provided tagNode.
146 */
147 private static boolean isAnotherTagBefore(DetailNode tagNode) {
148 boolean found = false;
149 DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
150 while (currentNode != null) {
151 if (currentNode.getType() == JavadocTokenTypes.JAVADOC_TAG) {
152 found = true;
153 break;
154 }
155 currentNode = JavadocUtil.getPreviousSibling(currentNode);
156 }
157 return found;
158 }
159
160 /**
161 * Returns true when there are is only whitespace and asterisks before the provided tagNode.
162 * When javadoc has only a javadoc tag like {@literal @} in it, the JAVADOC_TAG in a JAVADOC
163 * detail node will always have 2 or 3 siblings before it. The parse tree looks like:
164 * <pre>
165 * JAVADOC[3x0]
166 * |--NEWLINE[3x0] : [\n]
167 * |--LEADING_ASTERISK[4x0] : [ *]
168 * |--WS[4x2] : [ ]
169 * |--JAVADOC_TAG[4x3] : [@param T The bar.\n ]
170 * </pre>
171 * Or it can also look like:
172 * <pre>
173 * JAVADOC[3x0]
174 * |--NEWLINE[3x0] : [\n]
175 * |--LEADING_ASTERISK[4x0] : [ *]
176 * |--JAVADOC_TAG[4x3] : [@param T The bar.\n ]
177 * </pre>
178 * We do not include the variation
179 * <pre>
180 * /**@param noSpace there is no space here
181 * </pre>
182 * which results in the tree
183 * <pre>
184 * JAVADOC[3x0]
185 * |--JAVADOC_TAG[4x3] : [@param noSpace there is no space here\n ]
186 * </pre>
187 * because this one is invalid. We must recommend placing a blank line to separate @param
188 * from the first javadoc asterisks.
189 *
190 * @param tagNode the at tag node to check if there is nothing before it
191 * @return true if there is no text before the tagNode
192 */
193 private static boolean isOnlyTagInWholeJavadoc(DetailNode tagNode) {
194 final List<Integer> previousNodeTypes = new ArrayList<>();
195 DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
196 while (currentNode != null) {
197 previousNodeTypes.add(currentNode.getType());
198 currentNode = JavadocUtil.getPreviousSibling(currentNode);
199 }
200 return ONLY_TAG_VARIATION_1.equals(previousNodeTypes)
201 || ONLY_TAG_VARIATION_2.equals(previousNodeTypes);
202 }
203
204 /**
205 * Returns true when there are not enough empty lines before the provided tagNode.
206 *
207 * <p>Iterates through the previous siblings of the tagNode looking for empty lines until
208 * there are no more siblings or it hits something other than asterisk, whitespace or newline.
209 * If it finds at least one empty line, return true. Return false otherwise.</p>
210 *
211 * @param tagNode the tagNode to check if there are sufficient empty lines before it.
212 * @return true if there are not enough empty lines before the tagNode.
213 */
214 private static boolean hasInsufficientConsecutiveNewlines(DetailNode tagNode) {
215 int count = 0;
216 DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
217 while (currentNode != null
218 && (currentNode.getType() == JavadocTokenTypes.NEWLINE
219 || currentNode.getType() == JavadocTokenTypes.WS
220 || currentNode.getType() == JavadocTokenTypes.LEADING_ASTERISK)) {
221 if (currentNode.getType() == JavadocTokenTypes.NEWLINE) {
222 count++;
223 }
224 currentNode = JavadocUtil.getPreviousSibling(currentNode);
225 }
226
227 return count <= 1;
228 }
229 }