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