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