View Javadoc
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.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   * <div>
32   * Checks that there is no whitespace after a token.
33   * More specifically, it checks that it is not followed by whitespace,
34   * or (if linebreaks are allowed) all characters on the line after are
35   * whitespace. To forbid linebreaks after a token, set property
36   * {@code allowLineBreaks} to {@code false}.
37   * </div>
38   *
39   * <p>
40   * The check processes
41   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR">
42   * ARRAY_DECLARATOR</a> and
43   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP">
44   * INDEX_OP</a> tokens specially from other tokens. Actually it is checked that
45   * there is no whitespace before these tokens, not after them. Space after the
46   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATIONS">
47   * ANNOTATIONS</a> before
48   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR">
49   * ARRAY_DECLARATOR</a> and
50   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP">
51   * INDEX_OP</a> will be ignored.
52   * </p>
53   *
54   * <p>
55   * If the annotation is between the type and the array, like {@code char @NotNull [] param},
56   * the check will skip validation for spaces.
57   * </p>
58   *
59   * <p>
60   * Note: This check processes the
61   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_SYNCHRONIZED">
62   * LITERAL_SYNCHRONIZED</a> token only when it appears as a part of a
63   * <a href="https://docs.oracle.com/javase/specs/jls/se19/html/jls-14.html#jls-14.19">
64   * synchronized statement</a>, i.e. {@code synchronized(this) {}}.
65   * </p>
66   * <ul>
67   * <li>
68   * Property {@code allowLineBreaks} - Control whether whitespace is allowed
69   * if the token is at a linebreak.
70   * Type is {@code boolean}.
71   * Default value is {@code true}.
72   * </li>
73   * <li>
74   * Property {@code tokens} - tokens to check
75   * Type is {@code java.lang.String[]}.
76   * Validation type is {@code tokenSet}.
77   * Default value is:
78   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_INIT">
79   * ARRAY_INIT</a>,
80   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#AT">
81   * AT</a>,
82   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INC">
83   * INC</a>,
84   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#DEC">
85   * DEC</a>,
86   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#UNARY_MINUS">
87   * UNARY_MINUS</a>,
88   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#UNARY_PLUS">
89   * UNARY_PLUS</a>,
90   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BNOT">
91   * BNOT</a>,
92   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LNOT">
93   * LNOT</a>,
94   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#DOT">
95   * DOT</a>,
96   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR">
97   * ARRAY_DECLARATOR</a>,
98   * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP">
99   * INDEX_OP</a>.
100  * </li>
101  * </ul>
102  *
103  * <p>
104  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
105  * </p>
106  *
107  * <p>
108  * Violation Message Keys:
109  * </p>
110  * <ul>
111  * <li>
112  * {@code ws.followed}
113  * </li>
114  * </ul>
115  *
116  * @since 3.0
117  */
118 @StatelessCheck
119 public class NoWhitespaceAfterCheck extends AbstractCheck {
120 
121     /**
122      * A key is pointing to the warning message text in "messages.properties"
123      * file.
124      */
125     public static final String MSG_KEY = "ws.followed";
126 
127     /** Control whether whitespace is allowed if the token is at a linebreak. */
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      * Setter to control whether whitespace is allowed if the token is at a linebreak.
174      *
175      * @param allowLineBreaks whether whitespace should be
176      *     flagged at linebreaks.
177      * @since 3.0
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      * For a visited ast node returns node that should be checked
198      * for not being followed by whitespace.
199      *
200      * @param ast
201      *        , visited node.
202      * @return node before ast.
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      * Returns whether whitespace after a visited node should be checked. For example, whitespace
215      * is not allowed between a type and an array declarator (returns true), except when there is
216      * an annotation in between the type and array declarator (returns false).
217      *
218      * @param ast the visited node
219      * @return true if whitespace after ast should be checked
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      * Gets position after token (place of possible redundant whitespace).
231      *
232      * @param ast Node representing token.
233      * @return position after token.
234      */
235     private static int getPositionAfter(DetailAST ast) {
236         final int after;
237         // If target of possible redundant whitespace is in method definition.
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      * Checks if there is unwanted whitespace after the visited node.
253      *
254      * @param ast
255      *        , visited node.
256      * @param whitespaceColumnNo
257      *        , column number of a possible whitespace.
258      * @param whitespaceLineNo
259      *        , line number of a possible whitespace.
260      * @return true if whitespace found.
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      * Returns proper argument for getPositionAfter method, it is a token after
278      * {@link TokenTypes#ARRAY_DECLARATOR ARRAY_DECLARATOR}, in can be {@link TokenTypes#RBRACK
279      * RBRACK}, {@link TokenTypes#IDENT IDENT} or an array type definition (literal).
280      *
281      * @param ast
282      *        , {@link TokenTypes#ARRAY_DECLARATOR ARRAY_DECLARATOR} node.
283      * @return previous node by text order.
284      * @throws IllegalStateException if an unexpected token type is encountered.
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             // Covers higher dimension array declarations and initializations
292             previousElement = getPreviousElementOfMultiDimArray(ast);
293         }
294         else {
295             // First array index, is preceded with identifier or type
296             final DetailAST parent = ast.getParent();
297 
298             previousElement = switch (parent.getType()) {
299                 // Generics
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                 // Mundane array declaration, can be either Java style or C style
307                 case TokenTypes.TYPE -> getPreviousNodeWithParentOfTypeAst(ast, parent);
308 
309                 // Java 8 method reference
310                 case TokenTypes.METHOD_REF -> {
311                     final DetailAST ident = getIdentLastToken(ast);
312                     if (ident == null) {
313                         // i.e. int[]::new
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      * Gets the previous element of a second or higher dimension of an
328      * array declaration or initialization.
329      *
330      * @param leftBracket the token to get previous element of
331      * @return the previous element
332      */
333     private static DetailAST getPreviousElementOfMultiDimArray(DetailAST leftBracket) {
334         final DetailAST previousRightBracket = leftBracket.getPreviousSibling().getLastChild();
335 
336         DetailAST ident = null;
337         // This will get us past the type ident, to the actual identifier
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             // C style and Java style ' int[] arr []' in same construct
348             previousElement = ident;
349         }
350         else {
351             // 'int[][] arr' or 'int arr[][]'
352             previousElement = previousRightBracket;
353         }
354         return previousElement;
355     }
356 
357     /**
358      * Gets previous node for {@link TokenTypes#INDEX_OP INDEX_OP} token
359      * for usage in getPositionAfter method, it is a simplified copy of
360      * getArrayDeclaratorPreviousElement method.
361      *
362      * @param ast
363      *        , {@link TokenTypes#INDEX_OP INDEX_OP} node.
364      * @return previous node by text order.
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             // second or higher array index
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                 // construction like new int[]{1}[0]
381                 if (rparen == null) {
382                     final DetailAST lastChild = firstChild.getLastChild();
383                     result = lastChild.findFirstToken(TokenTypes.RCURLY);
384                 }
385                 // construction like ((byte[]) pixels)[0]
386                 else {
387                     result = rparen;
388                 }
389             }
390             else {
391                 result = ident;
392             }
393         }
394         return result;
395     }
396 
397     /**
398      * Searches parameter node for a type node.
399      * Returns it or its last node if it has an extended structure.
400      *
401      * @param ast
402      *        , subject node.
403      * @return type node.
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      * Finds previous node by text order for an array declarator,
428      * which parent type is {@link TokenTypes#TYPE TYPE}.
429      *
430      * @param ast
431      *        , array declarator node.
432      * @param parent
433      *        , its parent node.
434      * @return previous node by text order.
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         // sometimes there are ident-less sentences
441         // i.e. "(Object[]) null", but in casual case should be
442         // checked whether ident or lastTypeNode has preceding position
443         // determining if it is java style or C style
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         // ident and lastTypeNode lay on one line
452         else {
453             final int instanceOfSize = 13;
454             // +2 because ast has `[]` after the ident
455             if (ident.getColumnNo() >= ast.getColumnNo() + 2
456                 // +13 because ident (at most 1 character) is followed by
457                 // ' instanceof ' (12 characters)
458                 || lastTypeNode.getColumnNo() >= ident.getColumnNo() + instanceOfSize) {
459                 previousElement = lastTypeNode;
460             }
461             else {
462                 previousElement = ident;
463             }
464         }
465         return previousElement;
466     }
467 
468     /**
469      * Gets leftmost token of identifier.
470      *
471      * @param ast
472      *        , token possibly possessing an identifier.
473      * @return leftmost token of identifier.
474      */
475     private static DetailAST getIdentLastToken(DetailAST ast) {
476         final DetailAST result;
477         final Optional<DetailAST> dot = getPrecedingDot(ast);
478         // method call case
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         // qualified name case
489         else {
490             result = dot.orElseThrow().getFirstChild().getNextSibling();
491         }
492         return result;
493     }
494 
495     /**
496      * Gets the dot preceding a class member array index operation or class
497      * reference.
498      *
499      * @param leftBracket the ast we are checking
500      * @return dot preceding the left bracket
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      * Gets the dot preceding a class reference.
510      *
511      * @param leftBracket the ast we are checking
512      * @return dot preceding the left bracket
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 }