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.Optional;
23
24 import com.puppycrawl.tools.checkstyle.StatelessCheck;
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.CommonUtil;
29
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118 @StatelessCheck
119 public class NoWhitespaceAfterCheck extends AbstractCheck {
120
121
122
123
124
125 public static final String MSG_KEY = "ws.followed";
126
127
128 private boolean allowLineBreaks = true;
129
130 @Override
131 public int[] getDefaultTokens() {
132 return new int[] {
133 TokenTypes.ARRAY_INIT,
134 TokenTypes.AT,
135 TokenTypes.INC,
136 TokenTypes.DEC,
137 TokenTypes.UNARY_MINUS,
138 TokenTypes.UNARY_PLUS,
139 TokenTypes.BNOT,
140 TokenTypes.LNOT,
141 TokenTypes.DOT,
142 TokenTypes.ARRAY_DECLARATOR,
143 TokenTypes.INDEX_OP,
144 };
145 }
146
147 @Override
148 public int[] getAcceptableTokens() {
149 return new int[] {
150 TokenTypes.ARRAY_INIT,
151 TokenTypes.AT,
152 TokenTypes.INC,
153 TokenTypes.DEC,
154 TokenTypes.UNARY_MINUS,
155 TokenTypes.UNARY_PLUS,
156 TokenTypes.BNOT,
157 TokenTypes.LNOT,
158 TokenTypes.DOT,
159 TokenTypes.TYPECAST,
160 TokenTypes.ARRAY_DECLARATOR,
161 TokenTypes.INDEX_OP,
162 TokenTypes.LITERAL_SYNCHRONIZED,
163 TokenTypes.METHOD_REF,
164 };
165 }
166
167 @Override
168 public int[] getRequiredTokens() {
169 return CommonUtil.EMPTY_INT_ARRAY;
170 }
171
172
173
174
175
176
177
178
179 public void setAllowLineBreaks(boolean allowLineBreaks) {
180 this.allowLineBreaks = allowLineBreaks;
181 }
182
183 @Override
184 public void visitToken(DetailAST ast) {
185 if (shouldCheckWhitespaceAfter(ast)) {
186 final DetailAST whitespaceFollowedAst = getWhitespaceFollowedNode(ast);
187 final int whitespaceColumnNo = getPositionAfter(whitespaceFollowedAst);
188 final int whitespaceLineNo = whitespaceFollowedAst.getLineNo();
189
190 if (hasTrailingWhitespace(ast, whitespaceColumnNo, whitespaceLineNo)) {
191 log(ast, MSG_KEY, whitespaceFollowedAst.getText());
192 }
193 }
194 }
195
196
197
198
199
200
201
202
203
204 private static DetailAST getWhitespaceFollowedNode(DetailAST ast) {
205 return switch (ast.getType()) {
206 case TokenTypes.TYPECAST -> ast.findFirstToken(TokenTypes.RPAREN);
207 case TokenTypes.ARRAY_DECLARATOR -> getArrayDeclaratorPreviousElement(ast);
208 case TokenTypes.INDEX_OP -> getIndexOpPreviousElement(ast);
209 default -> ast;
210 };
211 }
212
213
214
215
216
217
218
219
220
221 private static boolean shouldCheckWhitespaceAfter(DetailAST ast) {
222 final DetailAST previousSibling = ast.getPreviousSibling();
223 final boolean isSynchronizedMethod = ast.getType() == TokenTypes.LITERAL_SYNCHRONIZED
224 && ast.getFirstChild() == null;
225 return !isSynchronizedMethod
226 && (previousSibling == null || previousSibling.getType() != TokenTypes.ANNOTATIONS);
227 }
228
229
230
231
232
233
234
235 private static int getPositionAfter(DetailAST ast) {
236 final int after;
237
238 if (ast.getType() == TokenTypes.IDENT
239 && ast.getNextSibling() != null
240 && ast.getNextSibling().getType() == TokenTypes.LPAREN) {
241 final DetailAST methodDef = ast.getParent();
242 final DetailAST endOfParams = methodDef.findFirstToken(TokenTypes.RPAREN);
243 after = endOfParams.getColumnNo() + 1;
244 }
245 else {
246 after = ast.getColumnNo() + ast.getText().length();
247 }
248 return after;
249 }
250
251
252
253
254
255
256
257
258
259
260
261
262 private boolean hasTrailingWhitespace(DetailAST ast,
263 int whitespaceColumnNo, int whitespaceLineNo) {
264 final boolean result;
265 final int astLineNo = ast.getLineNo();
266 final int[] line = getLineCodePoints(astLineNo - 1);
267 if (astLineNo == whitespaceLineNo && whitespaceColumnNo < line.length) {
268 result = CommonUtil.isCodePointWhitespace(line, whitespaceColumnNo);
269 }
270 else {
271 result = !allowLineBreaks;
272 }
273 return result;
274 }
275
276
277
278
279
280
281
282
283
284
285
286 private static DetailAST getArrayDeclaratorPreviousElement(DetailAST ast) {
287 final DetailAST previousElement;
288
289 if (ast.getPreviousSibling() != null
290 && ast.getPreviousSibling().getType() == TokenTypes.ARRAY_DECLARATOR) {
291
292 previousElement = getPreviousElementOfMultiDimArray(ast);
293 }
294 else {
295
296 final DetailAST parent = ast.getParent();
297
298 previousElement = switch (parent.getType()) {
299
300 case TokenTypes.TYPE_UPPER_BOUNDS, TokenTypes.TYPE_LOWER_BOUNDS ->
301 ast.getPreviousSibling();
302
303 case TokenTypes.LITERAL_NEW, TokenTypes.TYPE_ARGUMENT, TokenTypes.DOT ->
304 getTypeLastNode(ast);
305
306
307 case TokenTypes.TYPE -> getPreviousNodeWithParentOfTypeAst(ast, parent);
308
309
310 case TokenTypes.METHOD_REF -> {
311 final DetailAST ident = getIdentLastToken(ast);
312 if (ident == null) {
313
314 yield ast.getParent().getFirstChild();
315 }
316 yield ident;
317 }
318
319 default -> throw new IllegalStateException("unexpected ast syntax " + parent);
320 };
321 }
322
323 return previousElement;
324 }
325
326
327
328
329
330
331
332
333 private static DetailAST getPreviousElementOfMultiDimArray(DetailAST leftBracket) {
334 final DetailAST previousRightBracket = leftBracket.getPreviousSibling().getLastChild();
335
336 DetailAST ident = null;
337
338 DetailAST parent = leftBracket.getParent().getParent();
339 while (ident == null) {
340 ident = parent.findFirstToken(TokenTypes.IDENT);
341 parent = parent.getParent();
342 }
343
344 final DetailAST previousElement;
345 if (ident.getColumnNo() > previousRightBracket.getColumnNo()
346 && ident.getColumnNo() < leftBracket.getColumnNo()) {
347
348 previousElement = ident;
349 }
350 else {
351
352 previousElement = previousRightBracket;
353 }
354 return previousElement;
355 }
356
357
358
359
360
361
362
363
364
365
366 private static DetailAST getIndexOpPreviousElement(DetailAST ast) {
367 final DetailAST result;
368 final DetailAST firstChild = ast.getFirstChild();
369 if (firstChild.getType() == TokenTypes.INDEX_OP) {
370
371 result = firstChild.findFirstToken(TokenTypes.RBRACK);
372 }
373 else if (firstChild.getType() == TokenTypes.IDENT) {
374 result = firstChild;
375 }
376 else {
377 final DetailAST ident = getIdentLastToken(ast);
378 if (ident == null) {
379 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
380
381 if (rparen == null) {
382 final DetailAST lastChild = firstChild.getLastChild();
383 result = lastChild.findFirstToken(TokenTypes.RCURLY);
384 }
385
386 else {
387 result = rparen;
388 }
389 }
390 else {
391 result = ident;
392 }
393 }
394 return result;
395 }
396
397
398
399
400
401
402
403
404
405 private static DetailAST getTypeLastNode(DetailAST ast) {
406 final DetailAST typeLastNode;
407 final DetailAST parent = ast.getParent();
408 final boolean isPrecededByTypeArgs =
409 parent.findFirstToken(TokenTypes.TYPE_ARGUMENTS) != null;
410 final Optional<DetailAST> objectArrayType = Optional.ofNullable(getIdentLastToken(ast));
411
412 if (isPrecededByTypeArgs) {
413 typeLastNode = parent.findFirstToken(TokenTypes.TYPE_ARGUMENTS)
414 .findFirstToken(TokenTypes.GENERIC_END);
415 }
416 else if (objectArrayType.isPresent()) {
417 typeLastNode = objectArrayType.orElseThrow();
418 }
419 else {
420 typeLastNode = parent.getFirstChild();
421 }
422
423 return typeLastNode;
424 }
425
426
427
428
429
430
431
432
433
434
435
436 private static DetailAST getPreviousNodeWithParentOfTypeAst(DetailAST ast, DetailAST parent) {
437 final DetailAST previousElement;
438 final DetailAST ident = getIdentLastToken(parent.getParent());
439 final DetailAST lastTypeNode = getTypeLastNode(ast);
440
441
442
443
444
445 if (ident == null || ident.getLineNo() > ast.getLineNo()) {
446 previousElement = lastTypeNode;
447 }
448 else if (ident.getLineNo() < ast.getLineNo()) {
449 previousElement = ident;
450 }
451
452 else {
453 final int instanceOfSize = 13;
454
455 if (ident.getColumnNo() >= ast.getColumnNo() + 2
456
457
458 || lastTypeNode.getColumnNo() >= ident.getColumnNo() + instanceOfSize) {
459 previousElement = lastTypeNode;
460 }
461 else {
462 previousElement = ident;
463 }
464 }
465 return previousElement;
466 }
467
468
469
470
471
472
473
474
475 private static DetailAST getIdentLastToken(DetailAST ast) {
476 final DetailAST result;
477 final Optional<DetailAST> dot = getPrecedingDot(ast);
478
479 if (dot.isEmpty() || ast.getFirstChild().getType() == TokenTypes.METHOD_CALL) {
480 final DetailAST methodCall = ast.findFirstToken(TokenTypes.METHOD_CALL);
481 if (methodCall == null) {
482 result = ast.findFirstToken(TokenTypes.IDENT);
483 }
484 else {
485 result = methodCall.findFirstToken(TokenTypes.RPAREN);
486 }
487 }
488
489 else {
490 result = dot.orElseThrow().getFirstChild().getNextSibling();
491 }
492 return result;
493 }
494
495
496
497
498
499
500
501
502 private static Optional<DetailAST> getPrecedingDot(DetailAST leftBracket) {
503 final DetailAST referencedMemberDot = leftBracket.findFirstToken(TokenTypes.DOT);
504 final Optional<DetailAST> result = Optional.ofNullable(referencedMemberDot);
505 return result.or(() -> getReferencedClassDot(leftBracket));
506 }
507
508
509
510
511
512
513
514 private static Optional<DetailAST> getReferencedClassDot(DetailAST leftBracket) {
515 final DetailAST parent = leftBracket.getParent();
516 Optional<DetailAST> classDot = Optional.empty();
517 if (parent.getType() != TokenTypes.ASSIGN) {
518 classDot = Optional.ofNullable(parent.findFirstToken(TokenTypes.DOT));
519 }
520 return classDot;
521 }
522 }