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