1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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.whitespace;
21
22 import java.util.stream.IntStream;
23
24 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
25 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26 import com.puppycrawl.tools.checkstyle.api.DetailAST;
27 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28 import com.puppycrawl.tools.checkstyle.utils.CodePointUtil;
29 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30
31 /**
32 * <div>
33 * Checks that the whitespace around the Generic tokens (angle brackets)
34 * "<" and ">" are correct to the <i>typical</i> convention.
35 * The convention is not configurable.
36 * </div>
37 *
38 * <p>
39 * Left angle bracket ("<"):
40 * </p>
41 * <ul>
42 * <li> should be preceded with whitespace only
43 * in generic methods definitions.</li>
44 * <li> should not be preceded with whitespace
45 * when it is preceded method name or constructor.</li>
46 * <li> should not be preceded with whitespace when following type name.</li>
47 * <li> should not be followed with whitespace in all cases.</li>
48 * </ul>
49 *
50 * <p>
51 * Right angle bracket (">"):
52 * </p>
53 * <ul>
54 * <li> should not be preceded with whitespace in all cases.</li>
55 * <li> should be followed with whitespace in almost all cases,
56 * except diamond operators and when preceding a method name, constructor, or record header.</li>
57 * </ul>
58 *
59 * @since 5.0
60 */
61 @FileStatefulCheck
62 public class GenericWhitespaceCheck extends AbstractCheck {
63
64 /**
65 * A key is pointing to the warning message text in "messages.properties"
66 * file.
67 */
68 public static final String MSG_WS_PRECEDED = "ws.preceded";
69
70 /**
71 * A key is pointing to the warning message text in "messages.properties"
72 * file.
73 */
74 public static final String MSG_WS_FOLLOWED = "ws.followed";
75
76 /**
77 * A key is pointing to the warning message text in "messages.properties"
78 * file.
79 */
80 public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
81
82 /**
83 * A key is pointing to the warning message text in "messages.properties"
84 * file.
85 */
86 public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow";
87
88 /** Open angle bracket literal. */
89 private static final String OPEN_ANGLE_BRACKET = "<";
90
91 /** Close angle bracket literal. */
92 private static final String CLOSE_ANGLE_BRACKET = ">";
93
94 /** Used to count the depth of a Generic expression. */
95 private int depth;
96
97 @Override
98 public int[] getDefaultTokens() {
99 return getRequiredTokens();
100 }
101
102 @Override
103 public int[] getAcceptableTokens() {
104 return getRequiredTokens();
105 }
106
107 @Override
108 public int[] getRequiredTokens() {
109 return new int[] {TokenTypes.GENERIC_START, TokenTypes.GENERIC_END};
110 }
111
112 @Override
113 public void beginTree(DetailAST rootAST) {
114 // Reset for each tree, just increase there are violations in preceding
115 // trees.
116 depth = 0;
117 }
118
119 @Override
120 public void visitToken(DetailAST ast) {
121 switch (ast.getType()) {
122 case TokenTypes.GENERIC_START -> {
123 processStart(ast);
124 depth++;
125 }
126 case TokenTypes.GENERIC_END -> {
127 processEnd(ast);
128 depth--;
129 }
130 default -> throw new IllegalArgumentException("Unknown type " + ast);
131 }
132 }
133
134 /**
135 * Checks the token for the end of Generics.
136 *
137 * @param ast the token to check
138 */
139 private void processEnd(DetailAST ast) {
140 final int[] line = getLineCodePoints(ast.getLineNo() - 1);
141 final int before = ast.getColumnNo() - 1;
142 final int after = ast.getColumnNo() + 1;
143
144 if (before >= 0 && CommonUtil.isCodePointWhitespace(line, before)
145 && !containsWhitespaceBefore(before, line)) {
146 log(ast, MSG_WS_PRECEDED, CLOSE_ANGLE_BRACKET);
147 }
148
149 if (after < line.length) {
150 // Check if the last Generic, in which case must be a whitespace
151 // or a '(),[.'.
152 if (depth == 1) {
153 processSingleGeneric(ast, line, after);
154 }
155 else {
156 processNestedGenerics(ast, line, after);
157 }
158 }
159 }
160
161 /**
162 * Process Nested generics.
163 *
164 * @param ast token
165 * @param line unicode code points array of line
166 * @param after position after
167 */
168 private void processNestedGenerics(DetailAST ast, int[] line, int after) {
169 // In a nested Generic type, so can only be a '>' or ',' or '&'
170
171 // In case of several extends definitions:
172 //
173 // class IntEnumValueType<E extends Enum<E> & IntEnum>
174 // ^
175 // should be whitespace if followed by & -+
176 //
177 final int indexOfAmp = IntStream.range(after, line.length)
178 .filter(index -> line[index] == '&')
179 .findFirst()
180 .orElse(-1);
181 if (indexOfAmp >= 1
182 && containsWhitespaceBetween(after, indexOfAmp, line)) {
183 if (indexOfAmp - after == 0) {
184 log(ast, MSG_WS_NOT_PRECEDED, "&");
185 }
186 else if (indexOfAmp - after != 1) {
187 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
188 }
189 }
190 else if (line[after] == ' ') {
191 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
192 }
193 }
194
195 /**
196 * Process Single-generic.
197 *
198 * @param ast token
199 * @param line unicode code points array of line
200 * @param after position after
201 */
202 private void processSingleGeneric(DetailAST ast, int[] line, int after) {
203 final char charAfter = Character.toChars(line[after])[0];
204 if (isGenericBeforeMethod(ast)
205 || isGenericBeforeCtorInvocation(ast)
206 || isGenericBeforeRecordHeader(ast)) {
207 if (Character.isWhitespace(charAfter)) {
208 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
209 }
210 }
211 else if (!isCharacterValidAfterGenericEnd(charAfter)) {
212 log(ast, MSG_WS_ILLEGAL_FOLLOW, CLOSE_ANGLE_BRACKET);
213 }
214 }
215
216 /**
217 * Checks if generic is before record header. Identifies two cases:
218 * <ol>
219 * <li>In record def, eg: {@code record Session<T>()}</li>
220 * <li>In record pattern def, eg: {@code o instanceof Session<String>(var s)}</li>
221 * </ol>
222 *
223 * @param ast ast
224 * @return true if generic is before record header
225 */
226 private static boolean isGenericBeforeRecordHeader(DetailAST ast) {
227 final DetailAST grandParent = ast.getParent().getParent();
228 return grandParent.getType() == TokenTypes.RECORD_DEF
229 || grandParent.getParent().getType() == TokenTypes.RECORD_PATTERN_DEF;
230 }
231
232 /**
233 * Checks if generic is before constructor invocation. Identifies two cases:
234 * <ol>
235 * <li>{@code new ArrayList<>();}</li>
236 * <li>{@code new Outer.Inner<>();}</li>
237 * </ol>
238 *
239 * @param ast ast
240 * @return true if generic is before constructor invocation
241 */
242 private static boolean isGenericBeforeCtorInvocation(DetailAST ast) {
243 final DetailAST grandParent = ast.getParent().getParent();
244 return grandParent.getType() == TokenTypes.LITERAL_NEW
245 || grandParent.getParent().getType() == TokenTypes.LITERAL_NEW;
246 }
247
248 /**
249 * Checks if generic is after {@code LITERAL_NEW}. Identifies three cases:
250 * <ol>
251 * <li>{@code new <String>Object();}</li>
252 * <li>{@code new <String>Outer.Inner();}</li>
253 * <li>{@code new <@A Outer>@B Inner();}</li>
254 * </ol>
255 *
256 * @param ast ast
257 * @return true if generic after {@code LITERAL_NEW}
258 */
259 private static boolean isGenericAfterNew(DetailAST ast) {
260 final DetailAST parent = ast.getParent();
261 return parent.getParent().getType() == TokenTypes.LITERAL_NEW
262 && (parent.getNextSibling().getType() == TokenTypes.IDENT
263 || parent.getNextSibling().getType() == TokenTypes.DOT
264 || parent.getNextSibling().getType() == TokenTypes.ANNOTATIONS);
265 }
266
267 /**
268 * Is generic before method reference.
269 *
270 * @param ast ast
271 * @return true if generic before a method ref
272 */
273 private static boolean isGenericBeforeMethod(DetailAST ast) {
274 return ast.getParent().getParent().getParent().getType() == TokenTypes.METHOD_CALL
275 || isAfterMethodReference(ast);
276 }
277
278 /**
279 * Checks if current generic end ('>') is located after
280 * {@link TokenTypes#METHOD_REF method reference operator}.
281 *
282 * @param genericEnd {@link TokenTypes#GENERIC_END}
283 * @return true if '>' follows after method reference.
284 */
285 private static boolean isAfterMethodReference(DetailAST genericEnd) {
286 return genericEnd.getParent().getParent().getType() == TokenTypes.METHOD_REF;
287 }
288
289 /**
290 * Checks the token for the start of Generics.
291 *
292 * @param ast the token to check
293 */
294 private void processStart(DetailAST ast) {
295 final int[] line = getLineCodePoints(ast.getLineNo() - 1);
296 final int before = ast.getColumnNo() - 1;
297 final int after = ast.getColumnNo() + 1;
298
299 // Checks if generic needs to be preceded by a whitespace or not.
300 // Handles 3 cases as in:
301 //
302 // public static <T> Callable<T> callable(Runnable task, T result)
303 // ^ ^
304 // 1. ws reqd ---+ 2. +--- whitespace NOT required
305 //
306 // new <String>Object()
307 // ^
308 // 3. +--- ws required
309 if (before >= 0) {
310 final DetailAST parent = ast.getParent();
311 final DetailAST grandparent = parent.getParent();
312 // cases (1, 3) where whitespace is required:
313 if (grandparent.getType() == TokenTypes.CTOR_DEF
314 || grandparent.getType() == TokenTypes.METHOD_DEF
315 || isGenericAfterNew(ast)) {
316
317 if (!CommonUtil.isCodePointWhitespace(line, before)) {
318 log(ast, MSG_WS_NOT_PRECEDED, OPEN_ANGLE_BRACKET);
319 }
320 }
321 // case 2 where whitespace is not required:
322 else if (CommonUtil.isCodePointWhitespace(line, before)
323 && !containsWhitespaceBefore(before, line)) {
324 log(ast, MSG_WS_PRECEDED, OPEN_ANGLE_BRACKET);
325 }
326 }
327
328 if (after < line.length
329 && CommonUtil.isCodePointWhitespace(line, after)) {
330 log(ast, MSG_WS_FOLLOWED, OPEN_ANGLE_BRACKET);
331 }
332 }
333
334 /**
335 * Returns whether the specified string contains only whitespace between
336 * specified indices.
337 *
338 * @param fromIndex the index to start the search from. Inclusive
339 * @param toIndex the index to finish the search. Exclusive
340 * @param line the unicode code points array of line to check
341 * @return whether there are only whitespaces (or nothing)
342 */
343 private static boolean containsWhitespaceBetween(int fromIndex, int toIndex, int... line) {
344 boolean result = true;
345 for (int i = fromIndex; i < toIndex; i++) {
346 if (!CommonUtil.isCodePointWhitespace(line, i)) {
347 result = false;
348 break;
349 }
350 }
351 return result;
352 }
353
354 /**
355 * Returns whether the specified string contains only whitespace up to specified index.
356 *
357 * @param before the index to finish the search. Exclusive
358 * @param line the unicode code points array of line to check
359 * @return {@code true} if there are only whitespaces,
360 * false if there is nothing before or some other characters
361 */
362 private static boolean containsWhitespaceBefore(int before, int... line) {
363 return before != 0 && CodePointUtil.hasWhitespaceBefore(before, line);
364 }
365
366 /**
367 * Checks whether given character is valid to be right after generic ends.
368 *
369 * @param charAfter character to check
370 * @return checks if given character is valid
371 */
372 private static boolean isCharacterValidAfterGenericEnd(char charAfter) {
373 return charAfter == ')' || charAfter == ','
374 || charAfter == '[' || charAfter == '.'
375 || charAfter == ':' || charAfter == ';'
376 || Character.isWhitespace(charAfter);
377 }
378
379 }