001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.whitespace;
021
022import java.util.Optional;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
029
030/**
031 * <div>
032 * Checks that there is no whitespace after a token.
033 * More specifically, it checks that it is not followed by whitespace,
034 * or (if linebreaks are allowed) all characters on the line after are
035 * whitespace. To forbid linebreaks after a token, set property
036 * {@code allowLineBreaks} to {@code false}.
037 * </div>
038 *
039 * <p>
040 * The check processes
041 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR">
042 * ARRAY_DECLARATOR</a> and
043 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP">
044 * INDEX_OP</a> tokens specially from other tokens. Actually it is checked that
045 * there is no whitespace before these tokens, not after them. Space after the
046 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATIONS">
047 * ANNOTATIONS</a> before
048 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR">
049 * ARRAY_DECLARATOR</a> and
050 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP">
051 * INDEX_OP</a> will be ignored.
052 * </p>
053 *
054 * <p>
055 * If the annotation is between the type and the array, like {@code char @NotNull [] param},
056 * the check will skip validation for spaces.
057 * </p>
058 *
059 * <p>
060 * Note: This check processes the
061 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_SYNCHRONIZED">
062 * LITERAL_SYNCHRONIZED</a> token only when it appears as a part of a
063 * <a href="https://docs.oracle.com/javase/specs/jls/se19/html/jls-14.html#jls-14.19">
064 * synchronized statement</a>, i.e. {@code synchronized(this) {}}.
065 * </p>
066 * <ul>
067 * <li>
068 * Property {@code allowLineBreaks} - Control whether whitespace is allowed
069 * if the token is at a linebreak.
070 * Type is {@code boolean}.
071 * Default value is {@code true}.
072 * </li>
073 * <li>
074 * Property {@code tokens} - tokens to check
075 * Type is {@code java.lang.String[]}.
076 * Validation type is {@code tokenSet}.
077 * Default value is:
078 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_INIT">
079 * ARRAY_INIT</a>,
080 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#AT">
081 * AT</a>,
082 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INC">
083 * INC</a>,
084 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#DEC">
085 * DEC</a>,
086 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#UNARY_MINUS">
087 * UNARY_MINUS</a>,
088 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#UNARY_PLUS">
089 * UNARY_PLUS</a>,
090 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BNOT">
091 * BNOT</a>,
092 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LNOT">
093 * LNOT</a>,
094 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#DOT">
095 * DOT</a>,
096 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR">
097 * ARRAY_DECLARATOR</a>,
098 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP">
099 * 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
119public 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}