View Javadoc
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.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   *
67   * @since 3.0
68   */
69  @StatelessCheck
70  public class NoWhitespaceAfterCheck extends AbstractCheck {
71  
72      /**
73       * A key is pointing to the warning message text in "messages.properties"
74       * file.
75       */
76      public static final String MSG_KEY = "ws.followed";
77  
78      /** Control whether whitespace is allowed if the token is at a linebreak. */
79      private boolean allowLineBreaks = true;
80  
81      /**
82       * Creates a new {@code NoWhitespaceAfterCheck} instance.
83       */
84      public NoWhitespaceAfterCheck() {
85          // no code by default
86      }
87  
88      @Override
89      public int[] getDefaultTokens() {
90          return new int[] {
91              TokenTypes.ARRAY_INIT,
92              TokenTypes.AT,
93              TokenTypes.INC,
94              TokenTypes.DEC,
95              TokenTypes.UNARY_MINUS,
96              TokenTypes.UNARY_PLUS,
97              TokenTypes.BNOT,
98              TokenTypes.LNOT,
99              TokenTypes.DOT,
100             TokenTypes.ARRAY_DECLARATOR,
101             TokenTypes.INDEX_OP,
102         };
103     }
104 
105     @Override
106     public int[] getAcceptableTokens() {
107         return new int[] {
108             TokenTypes.ARRAY_INIT,
109             TokenTypes.AT,
110             TokenTypes.INC,
111             TokenTypes.DEC,
112             TokenTypes.UNARY_MINUS,
113             TokenTypes.UNARY_PLUS,
114             TokenTypes.BNOT,
115             TokenTypes.LNOT,
116             TokenTypes.DOT,
117             TokenTypes.TYPECAST,
118             TokenTypes.ARRAY_DECLARATOR,
119             TokenTypes.INDEX_OP,
120             TokenTypes.LITERAL_SYNCHRONIZED,
121             TokenTypes.METHOD_REF,
122         };
123     }
124 
125     @Override
126     public int[] getRequiredTokens() {
127         return CommonUtil.EMPTY_INT_ARRAY;
128     }
129 
130     /**
131      * Setter to control whether whitespace is allowed if the token is at a linebreak.
132      *
133      * @param allowLineBreaks whether whitespace should be
134      *     flagged at linebreaks.
135      * @since 3.0
136      */
137     public void setAllowLineBreaks(boolean allowLineBreaks) {
138         this.allowLineBreaks = allowLineBreaks;
139     }
140 
141     @Override
142     public void visitToken(DetailAST ast) {
143         if (shouldCheckWhitespaceAfter(ast)) {
144             final DetailAST whitespaceFollowedAst = getWhitespaceFollowedNode(ast);
145             final int whitespaceColumnNo = getPositionAfter(whitespaceFollowedAst);
146             final int whitespaceLineNo = whitespaceFollowedAst.getLineNo();
147 
148             if (hasTrailingWhitespace(ast, whitespaceColumnNo, whitespaceLineNo)) {
149                 log(ast, MSG_KEY, whitespaceFollowedAst.getText());
150             }
151         }
152     }
153 
154     /**
155      * For a visited ast node returns node that should be checked
156      * for not being followed by whitespace.
157      *
158      * @param ast
159      *        , visited node.
160      * @return node before ast.
161      */
162     private static DetailAST getWhitespaceFollowedNode(DetailAST ast) {
163         return switch (ast.getType()) {
164             case TokenTypes.TYPECAST -> ast.findFirstToken(TokenTypes.RPAREN);
165             case TokenTypes.ARRAY_DECLARATOR -> getArrayDeclaratorPreviousElement(ast);
166             case TokenTypes.INDEX_OP -> getIndexOpPreviousElement(ast);
167             default -> ast;
168         };
169     }
170 
171     /**
172      * Returns whether whitespace after a visited node should be checked. For example, whitespace
173      * is not allowed between a type and an array declarator (returns true), except when there is
174      * an annotation in between the type and array declarator (returns false).
175      *
176      * @param ast the visited node
177      * @return true if whitespace after ast should be checked
178      */
179     private static boolean shouldCheckWhitespaceAfter(DetailAST ast) {
180         final DetailAST previousSibling = ast.getPreviousSibling();
181         final boolean isSynchronizedMethod = ast.getType() == TokenTypes.LITERAL_SYNCHRONIZED
182                         && ast.getFirstChild() == null;
183         return !isSynchronizedMethod
184                 && (previousSibling == null || previousSibling.getType() != TokenTypes.ANNOTATIONS);
185     }
186 
187     /**
188      * Gets position after token (place of possible redundant whitespace).
189      *
190      * @param ast Node representing token.
191      * @return position after token.
192      */
193     private static int getPositionAfter(DetailAST ast) {
194         final int after;
195         // If target of possible redundant whitespace is in method definition.
196         if (ast.getType() == TokenTypes.IDENT
197                 && ast.getNextSibling() != null
198                 && ast.getNextSibling().getType() == TokenTypes.LPAREN) {
199             final DetailAST methodDef = ast.getParent();
200             final DetailAST endOfParams = methodDef.findFirstToken(TokenTypes.RPAREN);
201             after = endOfParams.getColumnNo() + 1;
202         }
203         else {
204             after = ast.getColumnNo() + ast.getText().length();
205         }
206         return after;
207     }
208 
209     /**
210      * Checks if there is unwanted whitespace after the visited node.
211      *
212      * @param ast
213      *        , visited node.
214      * @param whitespaceColumnNo
215      *        , column number of a possible whitespace.
216      * @param whitespaceLineNo
217      *        , line number of a possible whitespace.
218      * @return true if whitespace found.
219      */
220     private boolean hasTrailingWhitespace(DetailAST ast,
221         int whitespaceColumnNo, int whitespaceLineNo) {
222         final boolean result;
223         final int astLineNo = ast.getLineNo();
224         final int[] line = getLineCodePoints(astLineNo - 1);
225         if (astLineNo == whitespaceLineNo && whitespaceColumnNo < line.length) {
226             result = CommonUtil.isCodePointWhitespace(line, whitespaceColumnNo);
227         }
228         else {
229             result = !allowLineBreaks;
230         }
231         return result;
232     }
233 
234     /**
235      * Returns proper argument for getPositionAfter method, it is a token after
236      * {@link TokenTypes#ARRAY_DECLARATOR ARRAY_DECLARATOR}, in can be {@link TokenTypes#RBRACK
237      * RBRACK}, {@link TokenTypes#IDENT IDENT} or an array type definition (literal).
238      *
239      * @param ast
240      *        , {@code TokenTypes#ARRAY_DECLARATOR ARRAY_DECLARATOR} node.
241      * @return previous node by text order.
242      * @throws IllegalStateException if an unexpected token type is encountered.
243      */
244     private static DetailAST getArrayDeclaratorPreviousElement(DetailAST ast) {
245         final DetailAST previousElement;
246 
247         if (ast.getPreviousSibling() != null
248                 && ast.getPreviousSibling().getType() == TokenTypes.ARRAY_DECLARATOR) {
249             // Covers higher dimension array declarations and initializations
250             previousElement = getPreviousElementOfMultiDimArray(ast);
251         }
252         else {
253             // First array index, is preceded with identifier or type
254             final DetailAST parent = ast.getParent();
255 
256             previousElement = switch (parent.getType()) {
257                 // Generics
258                 case TokenTypes.TYPE_UPPER_BOUNDS, TokenTypes.TYPE_LOWER_BOUNDS ->
259                     ast.getPreviousSibling();
260 
261                 case TokenTypes.LITERAL_NEW, TokenTypes.TYPE_ARGUMENT, TokenTypes.DOT ->
262                     getTypeLastNode(ast);
263 
264                 // Mundane array declaration, can be either Java style or C style
265                 case TokenTypes.TYPE -> getPreviousNodeWithParentOfTypeAst(ast, parent);
266 
267                 // Java 8 method reference
268                 case TokenTypes.METHOD_REF -> {
269                     final DetailAST ident = getIdentLastToken(ast);
270                     if (ident == null) {
271                         // i.e. int[]::new
272                         yield ast.getParent().getFirstChild();
273                     }
274                     yield ident;
275                 }
276 
277                 default -> throw new IllegalStateException("unexpected ast syntax " + parent);
278             };
279         }
280 
281         return previousElement;
282     }
283 
284     /**
285      * Gets the previous element of a second or higher dimension of an
286      * array declaration or initialization.
287      *
288      * @param leftBracket the token to get previous element of
289      * @return the previous element
290      */
291     private static DetailAST getPreviousElementOfMultiDimArray(DetailAST leftBracket) {
292         final DetailAST previousRightBracket = leftBracket.getPreviousSibling().getLastChild();
293 
294         DetailAST ident = null;
295         // This will get us past the type ident, to the actual identifier
296         DetailAST parent = leftBracket.getParent().getParent();
297         while (ident == null) {
298             ident = parent.findFirstToken(TokenTypes.IDENT);
299             parent = parent.getParent();
300         }
301 
302         final DetailAST previousElement;
303         if (ident.getColumnNo() > previousRightBracket.getColumnNo()
304                 && ident.getColumnNo() < leftBracket.getColumnNo()) {
305             // C style and Java style ' int[] arr []' in same construct
306             previousElement = ident;
307         }
308         else {
309             // 'int[][] arr' or 'int arr[][]'
310             previousElement = previousRightBracket;
311         }
312         return previousElement;
313     }
314 
315     /**
316      * Gets previous node for {@link TokenTypes#INDEX_OP INDEX_OP} token
317      * for usage in getPositionAfter method, it is a simplified copy of
318      * getArrayDeclaratorPreviousElement method.
319      *
320      * @param ast
321      *        , {@code TokenTypes#INDEX_OP INDEX_OP} node.
322      * @return previous node by text order.
323      */
324     private static DetailAST getIndexOpPreviousElement(DetailAST ast) {
325         final DetailAST result;
326         final DetailAST firstChild = ast.getFirstChild();
327         if (firstChild.getType() == TokenTypes.INDEX_OP) {
328             // second or higher array index
329             result = firstChild.findFirstToken(TokenTypes.RBRACK);
330         }
331         else if (firstChild.getType() == TokenTypes.IDENT) {
332             result = firstChild;
333         }
334         else {
335             final DetailAST ident = getIdentLastToken(ast);
336             if (ident == null) {
337                 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
338                 // construction like new int[]{1}[0]
339                 if (rparen == null) {
340                     final DetailAST lastChild = firstChild.getLastChild();
341                     result = lastChild.findFirstToken(TokenTypes.RCURLY);
342                 }
343                 // construction like ((byte[]) pixels)[0]
344                 else {
345                     result = rparen;
346                 }
347             }
348             else {
349                 result = ident;
350             }
351         }
352         return result;
353     }
354 
355     /**
356      * Searches parameter node for a type node.
357      * Returns it or its last node if it has an extended structure.
358      *
359      * @param ast
360      *        , subject node.
361      * @return type node.
362      */
363     private static DetailAST getTypeLastNode(DetailAST ast) {
364         final DetailAST typeLastNode;
365         final DetailAST parent = ast.getParent();
366         final boolean isPrecededByTypeArgs =
367                 parent.findFirstToken(TokenTypes.TYPE_ARGUMENTS) != null;
368 
369         if (isPrecededByTypeArgs) {
370             typeLastNode = parent.findFirstToken(TokenTypes.TYPE_ARGUMENTS)
371                     .findFirstToken(TokenTypes.GENERIC_END);
372         }
373         else {
374             final Optional<DetailAST> objectArrayType = Optional.ofNullable(getIdentLastToken(ast));
375             typeLastNode = objectArrayType.orElseGet(parent::getFirstChild);
376         }
377 
378         return typeLastNode;
379     }
380 
381     /**
382      * Finds previous node by text order for an array declarator,
383      * which parent type is {@link TokenTypes#TYPE TYPE}.
384      *
385      * @param ast
386      *        , array declarator node.
387      * @param parent
388      *        , its parent node.
389      * @return previous node by text order.
390      */
391     private static DetailAST getPreviousNodeWithParentOfTypeAst(DetailAST ast, DetailAST parent) {
392         final DetailAST previousElement;
393         final DetailAST ident = getIdentLastToken(parent.getParent());
394         final DetailAST lastTypeNode = getTypeLastNode(ast);
395         // sometimes there are ident-less sentences
396         // i.e. "(Object[]) null", but in casual case should be
397         // checked whether ident or lastTypeNode has preceding position
398         // determining if it is java style or C style
399 
400         if (ident == null || ident.getLineNo() > ast.getLineNo()) {
401             previousElement = lastTypeNode;
402         }
403         else if (ident.getLineNo() < ast.getLineNo()) {
404             previousElement = ident;
405         }
406         // ident and lastTypeNode lay on one line
407         else {
408             final int instanceOfSize = 13;
409             // +2 because ast has `[]` after the ident
410             if (ident.getColumnNo() >= ast.getColumnNo() + 2
411                 // +13 because ident (at most 1 character) is followed by
412                 // ' instanceof ' (12 characters)
413                 || lastTypeNode.getColumnNo() >= ident.getColumnNo() + instanceOfSize) {
414                 previousElement = lastTypeNode;
415             }
416             else {
417                 previousElement = ident;
418             }
419         }
420         return previousElement;
421     }
422 
423     /**
424      * Gets leftmost token of identifier.
425      *
426      * @param ast
427      *        , token possibly possessing an identifier.
428      * @return leftmost token of identifier.
429      */
430     private static DetailAST getIdentLastToken(DetailAST ast) {
431         final DetailAST result;
432         final Optional<DetailAST> dot = getPrecedingDot(ast);
433         // method call case
434         if (dot.isEmpty() || ast.getFirstChild().getType() == TokenTypes.METHOD_CALL) {
435             final DetailAST methodCall = ast.findFirstToken(TokenTypes.METHOD_CALL);
436             if (methodCall == null) {
437                 result = ast.findFirstToken(TokenTypes.IDENT);
438             }
439             else {
440                 result = methodCall.findFirstToken(TokenTypes.RPAREN);
441             }
442         }
443         // qualified name case
444         else {
445             result = dot.orElseThrow().getFirstChild().getNextSibling();
446         }
447         return result;
448     }
449 
450     /**
451      * Gets the dot preceding a class member array index operation or class
452      * reference.
453      *
454      * @param leftBracket the ast we are checking
455      * @return dot preceding the left bracket
456      */
457     private static Optional<DetailAST> getPrecedingDot(DetailAST leftBracket) {
458         final DetailAST referencedMemberDot = leftBracket.findFirstToken(TokenTypes.DOT);
459         final Optional<DetailAST> result = Optional.ofNullable(referencedMemberDot);
460         return result.or(() -> getReferencedClassDot(leftBracket));
461     }
462 
463     /**
464      * Gets the dot preceding a class reference.
465      *
466      * @param leftBracket the ast we are checking
467      * @return dot preceding the left bracket
468      */
469     private static Optional<DetailAST> getReferencedClassDot(DetailAST leftBracket) {
470         final DetailAST parent = leftBracket.getParent();
471         Optional<DetailAST> classDot = Optional.empty();
472         if (parent.getType() != TokenTypes.ASSIGN) {
473             classDot = Optional.ofNullable(parent.findFirstToken(TokenTypes.DOT));
474         }
475         return classDot;
476     }
477 
478 }