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