001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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 com.puppycrawl.tools.checkstyle.StatelessCheck;
023import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.TokenTypes;
026import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
027
028/**
029 * <div>
030 * Checks that the whitespace around square-bracket tokens {@code [} and {@code ]}
031 * follows the Google Java Style Guide requirements for array declarations, array creation,
032 * and array indexing.
033 * </div>
034 *
035 * <p>
036 * Left square bracket ("{@code [}"):
037 * </p>
038 * <ul>
039 * <li>must not be preceded with whitespace when preceded by a
040 *   {@code TYPE} or {@code IDENT} in array declarations or array access</li>
041 * <li>must not be followed with whitespace</li>
042 * </ul>
043 *
044 * <p>
045 * Right square bracket ("{@code ]}"):
046 * </p>
047 * <ul>
048 * <li>must not be preceded with whitespace</li>
049 * <li>must be followed with whitespace in all cases, except when followed by:
050 *   <ul>
051 *     <li>another bracket: {@code [][]}</li>
052 *     <li>a dot for member access: {@code arr[i].length}</li>
053 *     <li>a comma or semicolon: {@code arr[i],} or {@code arr[i];}</li>
054 *     <li>postfix operators: {@code arr[i]++} or {@code arr[i]--}</li>
055 *     <li>a right parenthesis or another closing construct: {@code (arr[i])}</li>
056 *   </ul>
057 * </li>
058 * </ul>
059 *
060 * @since 13.1.0
061 */
062@StatelessCheck
063public class ArrayBracketNoWhitespaceCheck extends AbstractCheck {
064
065    /**
066     * A key is pointing to the warning message text in "messages.properties"
067     * file.
068     */
069    public static final String MSG_WS_PRECEDED = "ws.preceded";
070
071    /**
072     * A key is pointing to the warning message text in "messages.properties"
073     * file.
074     */
075    public static final String MSG_WS_FOLLOWED = "ws.followed";
076
077    /**
078     * A key is pointing to the warning message text in "messages.properties"
079     * file.
080     */
081    public static final String MSG_WS_NOT_FOLLOWED = "ws.notFollowed";
082
083    /** Open square bracket literal. */
084    private static final String LEFT_BRACKET = "[";
085
086    /** Close square bracket literal. */
087    private static final String RIGHT_BRACKET = "]";
088
089    @Override
090    public int[] getDefaultTokens() {
091        return getRequiredTokens();
092    }
093
094    @Override
095    public int[] getAcceptableTokens() {
096        return getRequiredTokens();
097    }
098
099    @Override
100    public int[] getRequiredTokens() {
101        return new int[] {
102            TokenTypes.ARRAY_DECLARATOR,
103            TokenTypes.INDEX_OP,
104        };
105    }
106
107    @Override
108    public void visitToken(DetailAST ast) {
109        // Check left bracket
110        if (precededByWhitespace(ast)) {
111            log(ast, MSG_WS_PRECEDED, LEFT_BRACKET);
112        }
113        if (followedByWhitespace(ast)) {
114            log(ast, MSG_WS_FOLLOWED, LEFT_BRACKET);
115        }
116
117        // Check right bracket
118        final DetailAST rightBracket = ast.findFirstToken(TokenTypes.RBRACK);
119        if (precededByWhitespace(rightBracket)) {
120            log(rightBracket, MSG_WS_PRECEDED, RIGHT_BRACKET);
121        }
122
123        // Check whitespace after right bracket
124        final int[] line = getLineCodePoints(rightBracket.getLineNo() - 1);
125        final int bracketEnd = rightBracket.getColumnNo() + 1;
126
127        if (bracketEnd < line.length) {
128            final DetailAST parent = rightBracket.getParent();
129            final DetailAST nextToken = findNextToken(parent, rightBracket);
130
131            if (nextToken != null) {
132                final boolean hasWhitespace = CommonUtil.isCodePointWhitespace(line, bracketEnd);
133                final boolean requiresWhitespace = !isValidWithoutWhitespace(nextToken);
134
135                if (requiresWhitespace && !hasWhitespace) {
136                    log(rightBracket, MSG_WS_NOT_FOLLOWED, RIGHT_BRACKET);
137                }
138                else if (!requiresWhitespace && hasWhitespace) {
139                    log(rightBracket, MSG_WS_FOLLOWED, RIGHT_BRACKET);
140                }
141            }
142        }
143    }
144
145    /**
146     * Checks if a token is preceded by whitespace.
147     *
148     * @param token the token to check
149     * @return true if preceded by whitespace, false otherwise
150     */
151    private boolean precededByWhitespace(DetailAST token) {
152        final int[] line = getLineCodePoints(token.getLineNo() - 1);
153        final int columnNo = token.getColumnNo();
154        final int before = columnNo - 1;
155
156        return before >= 0 && CommonUtil.isCodePointWhitespace(line, before);
157    }
158
159    /**
160     * Checks if a token is followed by whitespace.
161     *
162     * @param token the token to check
163     * @return true if followed by whitespace, false otherwise
164     */
165    private boolean followedByWhitespace(DetailAST token) {
166        final int[] line = getLineCodePoints(token.getLineNo() - 1);
167        final int columnNo = token.getColumnNo();
168        final int after = columnNo + 1;
169
170        return after < line.length && CommonUtil.isCodePointWhitespace(line, after);
171    }
172
173    /**
174     * Finds the next token after a right bracket by searching in a limited scope.
175     *
176     * @param arrayToken the array token (ARRAY_DECLARATOR or INDEX_OP)
177     * @param rightBracket the right bracket token for position checking
178     * @return the next token that appears after the bracket, or null if none found
179     */
180    private static DetailAST findNextToken(DetailAST arrayToken, DetailAST rightBracket) {
181        final int bracketLine = rightBracket.getLineNo();
182        final int bracketCol = rightBracket.getColumnNo();
183
184        // Traverse up a limited number of levels to find a reasonable search scope
185        DetailAST searchRoot = arrayToken;
186        final int maxLevels = 5;
187
188        for (int level = 0; level < maxLevels; level++) {
189            searchRoot = searchRoot.getParent();
190        }
191
192        return findClosestTokenAfter(searchRoot, bracketLine, bracketCol);
193    }
194
195    /**
196     * Recursively scans the AST to find the token closest to and after the given position.
197     *
198     * @param node the current node to scan
199     * @param line the line number of the bracket
200     * @param column the column number of the bracket
201     * @return the closest token after the position, or null if none found
202     */
203    private static DetailAST findClosestTokenAfter(DetailAST node, int line, int column) {
204        DetailAST closest = null;
205        int closestDistance = Integer.MAX_VALUE;
206
207        // Check current node (skip RBRACK and structural tokens)
208        if (isValidTokenToCheck(node)
209                && isTokenAfter(node, line, column) && node.getLineNo() == line) {
210            final int distance = node.getColumnNo() - column;
211            closest = node;
212            closestDistance = distance;
213        }
214
215        DetailAST child = node.getFirstChild();
216        while (child != null) {
217            final DetailAST candidate = findClosestTokenAfter(child, line, column);
218            if (candidate != null) {
219                final int distance = candidate.getColumnNo() - column;
220                if (distance < closestDistance) {
221                    closest = candidate;
222                    closestDistance = distance;
223                }
224            }
225
226            child = child.getNextSibling();
227        }
228
229        return closest;
230    }
231
232    /**
233     * Checks if a token is valid to consider as the "next" token.
234     * Filters out structural/container tokens that don't represent actual code.
235     *
236     * @param node the token to check
237     * @return true if this is a valid token to check
238     */
239    private static boolean isValidTokenToCheck(DetailAST node) {
240        final int type = node.getType();
241
242        return !isStructuralToken(type);
243    }
244
245    /**
246     * Checks if a token type is a structural/container token.
247     *
248     * @param type the token type to check
249     * @return true if this is a structural token
250     */
251    private static boolean isStructuralToken(int type) {
252        return type == TokenTypes.RBRACK
253                || isExpressionContainer(type)
254                || isTypeContainer(type)
255                || isDeclarationContainer(type);
256    }
257
258    /**
259     * Checks if a token type is an expression container.
260     *
261     * @param type the token type to check
262     * @return true if this is an expression container
263     */
264    private static boolean isExpressionContainer(int type) {
265        return type == TokenTypes.EXPR || type == TokenTypes.ELIST;
266    }
267
268    /**
269     * Checks if a token type is a type-related container.
270     *
271     * @param type the token type to check
272     * @return true if this is a type container
273     */
274    private static boolean isTypeContainer(int type) {
275        return type == TokenTypes.TYPE
276                || type == TokenTypes.TYPE_ARGUMENTS
277                || type == TokenTypes.TYPE_ARGUMENT;
278    }
279
280    /**
281     * Checks if a token type is a declaration/definition container.
282     *
283     * @param type the token type to check
284     * @return true if this is a declaration container
285     */
286    private static boolean isDeclarationContainer(int type) {
287        return type == TokenTypes.VARIABLE_DEF
288                || type == TokenTypes.MODIFIERS
289                || type == TokenTypes.ANNOTATIONS
290                || type == TokenTypes.OBJBLOCK;
291    }
292
293    /**
294     * Checks if a token appears after the given position in the source code.
295     *
296     * @param token the token to check
297     * @param line the line number to compare against
298     * @param column the column number to compare against
299     * @return true if the token is on a later line or same line but later column
300     */
301    private static boolean isTokenAfter(DetailAST token, int line, int column) {
302        return token.getLineNo() > line
303            || token.getLineNo() == line && token.getColumnNo() > column;
304    }
305
306    /**
307     * Checks if the given token can follow a right bracket without whitespace.
308     * Uses TokenTypes to determine valid tokens.
309     *
310     * @param nextToken the token that follows the right bracket
311     * @return true if the token can follow without whitespace
312     */
313    private static boolean isValidWithoutWhitespace(DetailAST nextToken) {
314        final int nextType = nextToken.getType();
315
316        return isBracketOrAccessor(nextType)
317                || isPunctuation(nextType)
318                || isPostfixOrShift(nextType);
319    }
320
321    /**
322     * Checks if a token type is a bracket or accessor.
323     *
324     * @param type the token type to check
325     * @return true if this is a bracket or accessor
326     */
327    private static boolean isBracketOrAccessor(int type) {
328        return type == TokenTypes.ARRAY_DECLARATOR
329                || type == TokenTypes.INDEX_OP
330                || type == TokenTypes.DOT
331                || type == TokenTypes.METHOD_REF;
332    }
333
334    /**
335     * Checks if a token type is punctuation that can follow without whitespace.
336     *
337     * @param type the token type to check
338     * @return true if this is valid punctuation
339     */
340    private static boolean isPunctuation(int type) {
341        return type == TokenTypes.COMMA
342                || type == TokenTypes.SEMI
343                || type == TokenTypes.RPAREN
344                || type == TokenTypes.RCURLY
345                || type == TokenTypes.GENERIC_END;
346    }
347
348    /**
349     * Checks if a token type is a postfix operator or shift operator.
350     *
351     * @param type the token type to check
352     * @return true if this is a postfix or shift operator
353     */
354    private static boolean isPostfixOrShift(int type) {
355        return type == TokenTypes.POST_INC
356                || type == TokenTypes.POST_DEC
357                || type == TokenTypes.SL
358                || type == TokenTypes.SR;
359    }
360}