1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 @FileStatefulCheck
84 public class GenericWhitespaceCheck extends AbstractCheck {
85
86
87
88
89
90 public static final String MSG_WS_PRECEDED = "ws.preceded";
91
92
93
94
95
96 public static final String MSG_WS_FOLLOWED = "ws.followed";
97
98
99
100
101
102 public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
103
104
105
106
107
108 public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow";
109
110
111 private static final String OPEN_ANGLE_BRACKET = "<";
112
113
114 private static final String CLOSE_ANGLE_BRACKET = ">";
115
116
117 private int depth;
118
119 @Override
120 public int[] getDefaultTokens() {
121 return getRequiredTokens();
122 }
123
124 @Override
125 public int[] getAcceptableTokens() {
126 return getRequiredTokens();
127 }
128
129 @Override
130 public int[] getRequiredTokens() {
131 return new int[] {TokenTypes.GENERIC_START, TokenTypes.GENERIC_END};
132 }
133
134 @Override
135 public void beginTree(DetailAST rootAST) {
136
137
138 depth = 0;
139 }
140
141 @Override
142 public void visitToken(DetailAST ast) {
143 switch (ast.getType()) {
144 case TokenTypes.GENERIC_START -> {
145 processStart(ast);
146 depth++;
147 }
148 case TokenTypes.GENERIC_END -> {
149 processEnd(ast);
150 depth--;
151 }
152 default -> throw new IllegalArgumentException("Unknown type " + ast);
153 }
154 }
155
156
157
158
159
160
161 private void processEnd(DetailAST ast) {
162 final int[] line = getLineCodePoints(ast.getLineNo() - 1);
163 final int before = ast.getColumnNo() - 1;
164 final int after = ast.getColumnNo() + 1;
165
166 if (before >= 0 && CommonUtil.isCodePointWhitespace(line, before)
167 && !containsWhitespaceBefore(before, line)) {
168 log(ast, MSG_WS_PRECEDED, CLOSE_ANGLE_BRACKET);
169 }
170
171 if (after < line.length) {
172
173
174 if (depth == 1) {
175 processSingleGeneric(ast, line, after);
176 }
177 else {
178 processNestedGenerics(ast, line, after);
179 }
180 }
181 }
182
183
184
185
186
187
188
189
190 private void processNestedGenerics(DetailAST ast, int[] line, int after) {
191
192
193
194
195
196
197
198
199 final int indexOfAmp = IntStream.range(after, line.length)
200 .filter(index -> line[index] == '&')
201 .findFirst()
202 .orElse(-1);
203 if (indexOfAmp >= 1
204 && containsWhitespaceBetween(after, indexOfAmp, line)) {
205 if (indexOfAmp - after == 0) {
206 log(ast, MSG_WS_NOT_PRECEDED, "&");
207 }
208 else if (indexOfAmp - after != 1) {
209 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
210 }
211 }
212 else if (line[after] == ' ') {
213 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
214 }
215 }
216
217
218
219
220
221
222
223
224 private void processSingleGeneric(DetailAST ast, int[] line, int after) {
225 final char charAfter = Character.toChars(line[after])[0];
226 if (isGenericBeforeMethod(ast)
227 || isGenericBeforeCtorInvocation(ast)
228 || isGenericBeforeRecordHeader(ast)) {
229 if (Character.isWhitespace(charAfter)) {
230 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET);
231 }
232 }
233 else if (!isCharacterValidAfterGenericEnd(charAfter)) {
234 log(ast, MSG_WS_ILLEGAL_FOLLOW, CLOSE_ANGLE_BRACKET);
235 }
236 }
237
238
239
240
241
242
243
244
245
246
247
248 private static boolean isGenericBeforeRecordHeader(DetailAST ast) {
249 final DetailAST grandParent = ast.getParent().getParent();
250 return grandParent.getType() == TokenTypes.RECORD_DEF
251 || grandParent.getParent().getType() == TokenTypes.RECORD_PATTERN_DEF;
252 }
253
254
255
256
257
258
259
260
261
262
263
264 private static boolean isGenericBeforeCtorInvocation(DetailAST ast) {
265 final DetailAST grandParent = ast.getParent().getParent();
266 return grandParent.getType() == TokenTypes.LITERAL_NEW
267 || grandParent.getParent().getType() == TokenTypes.LITERAL_NEW;
268 }
269
270
271
272
273
274
275
276
277
278
279
280
281 private static boolean isGenericAfterNew(DetailAST ast) {
282 final DetailAST parent = ast.getParent();
283 return parent.getParent().getType() == TokenTypes.LITERAL_NEW
284 && (parent.getNextSibling().getType() == TokenTypes.IDENT
285 || parent.getNextSibling().getType() == TokenTypes.DOT
286 || parent.getNextSibling().getType() == TokenTypes.ANNOTATIONS);
287 }
288
289
290
291
292
293
294
295 private static boolean isGenericBeforeMethod(DetailAST ast) {
296 return ast.getParent().getParent().getParent().getType() == TokenTypes.METHOD_CALL
297 || isAfterMethodReference(ast);
298 }
299
300
301
302
303
304
305
306
307 private static boolean isAfterMethodReference(DetailAST genericEnd) {
308 return genericEnd.getParent().getParent().getType() == TokenTypes.METHOD_REF;
309 }
310
311
312
313
314
315
316 private void processStart(DetailAST ast) {
317 final int[] line = getLineCodePoints(ast.getLineNo() - 1);
318 final int before = ast.getColumnNo() - 1;
319 final int after = ast.getColumnNo() + 1;
320
321
322
323
324
325
326
327
328
329
330
331 if (before >= 0) {
332 final DetailAST parent = ast.getParent();
333 final DetailAST grandparent = parent.getParent();
334
335 if (grandparent.getType() == TokenTypes.CTOR_DEF
336 || grandparent.getType() == TokenTypes.METHOD_DEF
337 || isGenericAfterNew(ast)) {
338
339 if (!CommonUtil.isCodePointWhitespace(line, before)) {
340 log(ast, MSG_WS_NOT_PRECEDED, OPEN_ANGLE_BRACKET);
341 }
342 }
343
344 else if (CommonUtil.isCodePointWhitespace(line, before)
345 && !containsWhitespaceBefore(before, line)) {
346 log(ast, MSG_WS_PRECEDED, OPEN_ANGLE_BRACKET);
347 }
348 }
349
350 if (after < line.length
351 && CommonUtil.isCodePointWhitespace(line, after)) {
352 log(ast, MSG_WS_FOLLOWED, OPEN_ANGLE_BRACKET);
353 }
354 }
355
356
357
358
359
360
361
362
363
364
365 private static boolean containsWhitespaceBetween(int fromIndex, int toIndex, int... line) {
366 boolean result = true;
367 for (int i = fromIndex; i < toIndex; i++) {
368 if (!CommonUtil.isCodePointWhitespace(line, i)) {
369 result = false;
370 break;
371 }
372 }
373 return result;
374 }
375
376
377
378
379
380
381
382
383
384 private static boolean containsWhitespaceBefore(int before, int... line) {
385 return before != 0 && CodePointUtil.hasWhitespaceBefore(before, line);
386 }
387
388
389
390
391
392
393
394 private static boolean isCharacterValidAfterGenericEnd(char charAfter) {
395 return charAfter == ')' || charAfter == ','
396 || charAfter == '[' || charAfter == '.'
397 || charAfter == ':' || charAfter == ';'
398 || Character.isWhitespace(charAfter);
399 }
400
401 }