View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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         final DetailAST whitespaceFollowedAst;
206         switch (ast.getType()) {
207             case TokenTypes.TYPECAST:
208                 whitespaceFollowedAst = ast.findFirstToken(TokenTypes.RPAREN);
209                 break;
210             case TokenTypes.ARRAY_DECLARATOR:
211                 whitespaceFollowedAst = getArrayDeclaratorPreviousElement(ast);
212                 break;
213             case TokenTypes.INDEX_OP:
214                 whitespaceFollowedAst = getIndexOpPreviousElement(ast);
215                 break;
216             default:
217                 whitespaceFollowedAst = ast;
218         }
219         return whitespaceFollowedAst;
220     }
221 
222     /**
223      * Returns whether whitespace after a visited node should be checked. For example, whitespace
224      * is not allowed between a type and an array declarator (returns true), except when there is
225      * an annotation in between the type and array declarator (returns false).
226      *
227      * @param ast the visited node
228      * @return true if whitespace after ast should be checked
229      */
230     private static boolean shouldCheckWhitespaceAfter(DetailAST ast) {
231         final DetailAST previousSibling = ast.getPreviousSibling();
232         final boolean isSynchronizedMethod = ast.getType() == TokenTypes.LITERAL_SYNCHRONIZED
233                         && ast.getFirstChild() == null;
234         return !isSynchronizedMethod
235                 && (previousSibling == null || previousSibling.getType() != TokenTypes.ANNOTATIONS);
236     }
237 
238     /**
239      * Gets position after token (place of possible redundant whitespace).
240      *
241      * @param ast Node representing token.
242      * @return position after token.
243      */
244     private static int getPositionAfter(DetailAST ast) {
245         final int after;
246         // If target of possible redundant whitespace is in method definition.
247         if (ast.getType() == TokenTypes.IDENT
248                 && ast.getNextSibling() != null
249                 && ast.getNextSibling().getType() == TokenTypes.LPAREN) {
250             final DetailAST methodDef = ast.getParent();
251             final DetailAST endOfParams = methodDef.findFirstToken(TokenTypes.RPAREN);
252             after = endOfParams.getColumnNo() + 1;
253         }
254         else {
255             after = ast.getColumnNo() + ast.getText().length();
256         }
257         return after;
258     }
259 
260     /**
261      * Checks if there is unwanted whitespace after the visited node.
262      *
263      * @param ast
264      *        , visited node.
265      * @param whitespaceColumnNo
266      *        , column number of a possible whitespace.
267      * @param whitespaceLineNo
268      *        , line number of a possible whitespace.
269      * @return true if whitespace found.
270      */
271     private boolean hasTrailingWhitespace(DetailAST ast,
272         int whitespaceColumnNo, int whitespaceLineNo) {
273         final boolean result;
274         final int astLineNo = ast.getLineNo();
275         final int[] line = getLineCodePoints(astLineNo - 1);
276         if (astLineNo == whitespaceLineNo && whitespaceColumnNo < line.length) {
277             result = CommonUtil.isCodePointWhitespace(line, whitespaceColumnNo);
278         }
279         else {
280             result = !allowLineBreaks;
281         }
282         return result;
283     }
284 
285     /**
286      * Returns proper argument for getPositionAfter method, it is a token after
287      * {@link TokenTypes#ARRAY_DECLARATOR ARRAY_DECLARATOR}, in can be {@link TokenTypes#RBRACK
288      * RBRACK}, {@link TokenTypes#IDENT IDENT} or an array type definition (literal).
289      *
290      * @param ast
291      *        , {@link TokenTypes#ARRAY_DECLARATOR ARRAY_DECLARATOR} node.
292      * @return previous node by text order.
293      * @throws IllegalStateException if an unexpected token type is encountered.
294      */
295     private static DetailAST getArrayDeclaratorPreviousElement(DetailAST ast) {
296         final DetailAST previousElement;
297 
298         if (ast.getPreviousSibling() != null
299                 && ast.getPreviousSibling().getType() == TokenTypes.ARRAY_DECLARATOR) {
300             // Covers higher dimension array declarations and initializations
301             previousElement = getPreviousElementOfMultiDimArray(ast);
302         }
303         else {
304             // first array index, is preceded with identifier or type
305             final DetailAST parent = ast.getParent();
306             switch (parent.getType()) {
307                 // generics
308                 case TokenTypes.TYPE_UPPER_BOUNDS:
309                 case TokenTypes.TYPE_LOWER_BOUNDS:
310                     previousElement = ast.getPreviousSibling();
311                     break;
312                 case TokenTypes.LITERAL_NEW:
313                 case TokenTypes.TYPE_ARGUMENT:
314                 case TokenTypes.DOT:
315                     previousElement = getTypeLastNode(ast);
316                     break;
317                 // mundane array declaration, can be either java style or C style
318                 case TokenTypes.TYPE:
319                     previousElement = getPreviousNodeWithParentOfTypeAst(ast, parent);
320                     break;
321                 // java 8 method reference
322                 case TokenTypes.METHOD_REF:
323                     final DetailAST ident = getIdentLastToken(ast);
324                     if (ident == null) {
325                         // i.e. int[]::new
326                         previousElement = ast.getParent().getFirstChild();
327                     }
328                     else {
329                         previousElement = ident;
330                     }
331                     break;
332                 default:
333                     throw new IllegalStateException("unexpected ast syntax " + parent);
334             }
335         }
336         return previousElement;
337     }
338 
339     /**
340      * Gets the previous element of a second or higher dimension of an
341      * array declaration or initialization.
342      *
343      * @param leftBracket the token to get previous element of
344      * @return the previous element
345      */
346     private static DetailAST getPreviousElementOfMultiDimArray(DetailAST leftBracket) {
347         final DetailAST previousRightBracket = leftBracket.getPreviousSibling().getLastChild();
348 
349         DetailAST ident = null;
350         // This will get us past the type ident, to the actual identifier
351         DetailAST parent = leftBracket.getParent().getParent();
352         while (ident == null) {
353             ident = parent.findFirstToken(TokenTypes.IDENT);
354             parent = parent.getParent();
355         }
356 
357         final DetailAST previousElement;
358         if (ident.getColumnNo() > previousRightBracket.getColumnNo()
359                 && ident.getColumnNo() < leftBracket.getColumnNo()) {
360             // C style and Java style ' int[] arr []' in same construct
361             previousElement = ident;
362         }
363         else {
364             // 'int[][] arr' or 'int arr[][]'
365             previousElement = previousRightBracket;
366         }
367         return previousElement;
368     }
369 
370     /**
371      * Gets previous node for {@link TokenTypes#INDEX_OP INDEX_OP} token
372      * for usage in getPositionAfter method, it is a simplified copy of
373      * getArrayDeclaratorPreviousElement method.
374      *
375      * @param ast
376      *        , {@link TokenTypes#INDEX_OP INDEX_OP} node.
377      * @return previous node by text order.
378      */
379     private static DetailAST getIndexOpPreviousElement(DetailAST ast) {
380         final DetailAST result;
381         final DetailAST firstChild = ast.getFirstChild();
382         if (firstChild.getType() == TokenTypes.INDEX_OP) {
383             // second or higher array index
384             result = firstChild.findFirstToken(TokenTypes.RBRACK);
385         }
386         else if (firstChild.getType() == TokenTypes.IDENT) {
387             result = firstChild;
388         }
389         else {
390             final DetailAST ident = getIdentLastToken(ast);
391             if (ident == null) {
392                 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
393                 // construction like new int[]{1}[0]
394                 if (rparen == null) {
395                     final DetailAST lastChild = firstChild.getLastChild();
396                     result = lastChild.findFirstToken(TokenTypes.RCURLY);
397                 }
398                 // construction like ((byte[]) pixels)[0]
399                 else {
400                     result = rparen;
401                 }
402             }
403             else {
404                 result = ident;
405             }
406         }
407         return result;
408     }
409 
410     /**
411      * Searches parameter node for a type node.
412      * Returns it or its last node if it has an extended structure.
413      *
414      * @param ast
415      *        , subject node.
416      * @return type node.
417      */
418     private static DetailAST getTypeLastNode(DetailAST ast) {
419         final DetailAST typeLastNode;
420         final DetailAST parent = ast.getParent();
421         final boolean isPrecededByTypeArgs =
422                 parent.findFirstToken(TokenTypes.TYPE_ARGUMENTS) != null;
423         final Optional<DetailAST> objectArrayType = Optional.ofNullable(getIdentLastToken(ast));
424 
425         if (isPrecededByTypeArgs) {
426             typeLastNode = parent.findFirstToken(TokenTypes.TYPE_ARGUMENTS)
427                     .findFirstToken(TokenTypes.GENERIC_END);
428         }
429         else if (objectArrayType.isPresent()) {
430             typeLastNode = objectArrayType.orElseThrow();
431         }
432         else {
433             typeLastNode = parent.getFirstChild();
434         }
435 
436         return typeLastNode;
437     }
438 
439     /**
440      * Finds previous node by text order for an array declarator,
441      * which parent type is {@link TokenTypes#TYPE TYPE}.
442      *
443      * @param ast
444      *        , array declarator node.
445      * @param parent
446      *        , its parent node.
447      * @return previous node by text order.
448      */
449     private static DetailAST getPreviousNodeWithParentOfTypeAst(DetailAST ast, DetailAST parent) {
450         final DetailAST previousElement;
451         final DetailAST ident = getIdentLastToken(parent.getParent());
452         final DetailAST lastTypeNode = getTypeLastNode(ast);
453         // sometimes there are ident-less sentences
454         // i.e. "(Object[]) null", but in casual case should be
455         // checked whether ident or lastTypeNode has preceding position
456         // determining if it is java style or C style
457 
458         if (ident == null || ident.getLineNo() > ast.getLineNo()) {
459             previousElement = lastTypeNode;
460         }
461         else if (ident.getLineNo() < ast.getLineNo()) {
462             previousElement = ident;
463         }
464         // ident and lastTypeNode lay on one line
465         else {
466             final int instanceOfSize = 13;
467             // +2 because ast has `[]` after the ident
468             if (ident.getColumnNo() >= ast.getColumnNo() + 2
469                 // +13 because ident (at most 1 character) is followed by
470                 // ' instanceof ' (12 characters)
471                 || lastTypeNode.getColumnNo() >= ident.getColumnNo() + instanceOfSize) {
472                 previousElement = lastTypeNode;
473             }
474             else {
475                 previousElement = ident;
476             }
477         }
478         return previousElement;
479     }
480 
481     /**
482      * Gets leftmost token of identifier.
483      *
484      * @param ast
485      *        , token possibly possessing an identifier.
486      * @return leftmost token of identifier.
487      */
488     private static DetailAST getIdentLastToken(DetailAST ast) {
489         final DetailAST result;
490         final Optional<DetailAST> dot = getPrecedingDot(ast);
491         // method call case
492         if (dot.isEmpty() || ast.getFirstChild().getType() == TokenTypes.METHOD_CALL) {
493             final DetailAST methodCall = ast.findFirstToken(TokenTypes.METHOD_CALL);
494             if (methodCall == null) {
495                 result = ast.findFirstToken(TokenTypes.IDENT);
496             }
497             else {
498                 result = methodCall.findFirstToken(TokenTypes.RPAREN);
499             }
500         }
501         // qualified name case
502         else {
503             result = dot.orElseThrow().getFirstChild().getNextSibling();
504         }
505         return result;
506     }
507 
508     /**
509      * Gets the dot preceding a class member array index operation or class
510      * reference.
511      *
512      * @param leftBracket the ast we are checking
513      * @return dot preceding the left bracket
514      */
515     private static Optional<DetailAST> getPrecedingDot(DetailAST leftBracket) {
516         final DetailAST referencedMemberDot = leftBracket.findFirstToken(TokenTypes.DOT);
517         final Optional<DetailAST> result = Optional.ofNullable(referencedMemberDot);
518         return result.or(() -> getReferencedClassDot(leftBracket));
519     }
520 
521     /**
522      * Gets the dot preceding a class reference.
523      *
524      * @param leftBracket the ast we are checking
525      * @return dot preceding the left bracket
526      */
527     private static Optional<DetailAST> getReferencedClassDot(DetailAST leftBracket) {
528         final DetailAST parent = leftBracket.getParent();
529         Optional<DetailAST> classDot = Optional.empty();
530         if (parent.getType() != TokenTypes.ASSIGN) {
531             classDot = Optional.ofNullable(parent.findFirstToken(TokenTypes.DOT));
532         }
533         return classDot;
534     }
535 }