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