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