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 java.util.Set;
023
024import javax.annotation.Nullable;
025
026import com.puppycrawl.tools.checkstyle.StatelessCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
031
032/**
033 * <div>
034 * Checks that the whitespace around square-bracket tokens {@code [} and {@code ]}
035 * follows the Google Java Style Guide requirements for array declarations, array creation,
036 * and array indexing.
037 * </div>
038 *
039 * <p>
040 * Left square bracket ("{@code [}"):
041 * </p>
042 * <ul>
043 *   <li>must not be preceded with whitespace when preceded by a
044 *     {@code TYPE} or {@code IDENT} in array declarations or array access</li>
045 *   <li>must not be followed with whitespace</li>
046 * </ul>
047 *
048 * <p>
049 * Right square bracket ("{@code ]}"):
050 * </p>
051 * <ul>
052 *   <li>must not be preceded with whitespace</li>
053 *   <li>must be followed with whitespace in all cases, except when followed by:
054 *   <ul>
055 *     <li>another bracket: {@code [][]}</li>
056 *     <li>a dot for member access: {@code arr[i].length}</li>
057 *     <li>a comma or semicolon: {@code arr[i],} or {@code arr[i];}</li>
058 *     <li>postfix operators: {@code arr[i]++} or {@code arr[i]--}</li>
059 *     <li>a right parenthesis or another closing construct: {@code (arr[i])}</li>
060 *   </ul>
061 * </li>
062 * </ul>
063 *
064 * @since 13.10.0
065 */
066@StatelessCheck
067public class ArrayBracketNoWhitespaceCheck extends AbstractCheck {
068
069    /**
070     * A key is pointing to the warning message text in "messages.properties"
071     * file.
072     */
073    public static final String MSG_WS_PRECEDED = "ws.preceded";
074
075    /**
076     * A key is pointing to the warning message text in "messages.properties"
077     * file.
078     */
079    public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
080
081    /**
082     * A key is pointing to the warning message text in "messages.properties"
083     * file.
084     */
085    public static final String MSG_WS_FOLLOWED = "ws.followed";
086
087    /**
088     * A key is pointing to the warning message text in "messages.properties"
089     * file.
090     */
091    public static final String MSG_WS_NOT_FOLLOWED = "ws.notFollowed";
092
093    /**
094     * Tokens that are valid after a right bracket without whitespace.
095     */
096    private static final Set<Integer> VALID_AFTER_RIGHT_BRACKET_TOKENS =
097        Set.of(
098            TokenTypes.ARRAY_DECLARATOR,
099            TokenTypes.INDEX_OP,
100            TokenTypes.DOT,
101            TokenTypes.METHOD_REF,
102            TokenTypes.RBRACK,
103            TokenTypes.RPAREN,
104            TokenTypes.RCURLY,
105            TokenTypes.COMMA,
106            TokenTypes.SEMI,
107            TokenTypes.GENERIC_END,
108            TokenTypes.POST_INC,
109            TokenTypes.POST_DEC,
110            TokenTypes.ELLIPSIS
111        );
112
113    /**
114     * Creates a new {@code ArrayBracketNoWhitespaceCheck} instance.
115     */
116    public ArrayBracketNoWhitespaceCheck() {
117        // no code by default
118    }
119
120    @Override
121    public int[] getDefaultTokens() {
122        return getRequiredTokens();
123    }
124
125    @Override
126    public int[] getAcceptableTokens() {
127        return getRequiredTokens();
128    }
129
130    @Override
131    public int[] getRequiredTokens() {
132        return new int[] {
133            TokenTypes.ARRAY_DECLARATOR,
134            TokenTypes.INDEX_OP,
135            TokenTypes.RBRACK,
136        };
137    }
138
139    @Override
140    public void visitToken(DetailAST ast) {
141        if (ast.getType() == TokenTypes.RBRACK) {
142            processRightBracket(ast);
143        }
144        else {
145            final boolean whitespaceBefore = isWhitespaceAt(ast, ast.getColumnNo() - 1);
146            final boolean annotationBefore = isPrecededByAnnotation(ast);
147            if (!annotationBefore && whitespaceBefore) {
148                log(ast, MSG_WS_PRECEDED, ast.getText());
149            }
150            else if (annotationBefore && !whitespaceBefore) {
151                log(ast, MSG_WS_NOT_PRECEDED, ast.getText());
152            }
153            if (isWhitespaceAt(ast, ast.getColumnNo() + 1)) {
154                log(ast, MSG_WS_FOLLOWED, ast.getText());
155            }
156        }
157    }
158
159    /**
160     * Processes a right bracket token and logs violations if it is preceded
161     * or followed by whitespace inappropriately.
162     *
163     * @param ast the right bracket token to process
164     */
165    private void processRightBracket(DetailAST ast) {
166        if (isWhitespaceAt(ast, ast.getColumnNo() - 1)) {
167            log(ast, MSG_WS_PRECEDED, ast.getText());
168        }
169
170        final DetailAST nextToken = findNextToken(ast);
171        if (nextToken != null) {
172            final boolean whitespaceAfter = isWhitespaceAt(ast, ast.getColumnNo() + 1);
173            final boolean requiresWhitespace = !isValidWithoutWhitespace(nextToken);
174            if (requiresWhitespace && !whitespaceAfter) {
175                log(ast, MSG_WS_NOT_FOLLOWED, ast.getText());
176            }
177            else if (!requiresWhitespace && whitespaceAfter) {
178                log(ast, MSG_WS_FOLLOWED, ast.getText());
179            }
180        }
181    }
182
183    /**
184     * Checks whether an {@code ARRAY_DECLARATOR} is immediately preceded by an
185     * {@code ANNOTATIONS} sibling, which happens in constructs like
186     * {@code int @Ann [] x}.
187     *
188     * @param ast the {@code ARRAY_DECLARATOR} or {@code INDEX_OP} token
189     * @return true if the token's previous sibling is an ANNOTATIONS node
190     */
191    private static boolean isPrecededByAnnotation(DetailAST ast) {
192        final DetailAST previousSibling = ast.getPreviousSibling();
193        return previousSibling != null
194                && previousSibling.getType() == TokenTypes.ANNOTATIONS;
195    }
196
197    /**
198     * Checks if a whitespace character is present at the given column on the
199     * same line as the provided token.
200     *
201     * @param token the token whose line should be checked
202     * @param columnNo the column number to inspect for whitespace
203     * @return true if the character at {@code columnNo} is a whitespace character
204     */
205    private boolean isWhitespaceAt(DetailAST token, int columnNo) {
206        final int[] line = getLineCodePoints(token.getLineNo() - 1);
207        return columnNo >= 0 && columnNo < line.length
208                && CommonUtil.isCodePointWhitespace(line, columnNo);
209    }
210
211    /**
212     * Finds the next token after a right bracket by climbing the AST and
213     * scanning next-sibling chains at each level. At every level all siblings
214     * are visited and passed to {@link #findBestCandidate}.
215     * The candidate with the smallest qualifying column is returned.
216     *
217     * @param rightBracket the right bracket token whose successor is needed
218     * @return the closest same-line token that follows the bracket, or {@code null}
219     *         if no such token exists on that line
220     */
221    @Nullable
222    private static DetailAST findNextToken(DetailAST rightBracket) {
223        DetailAST candidate = null;
224        DetailAST current = rightBracket;
225
226        while (current != null) {
227            for (DetailAST sibling = current; sibling != null; sibling = sibling.getNextSibling()) {
228                candidate = findBestCandidate(candidate, rightBracket, sibling);
229            }
230            current = current.getParent();
231        }
232        return candidate;
233    }
234
235    /**
236     * Evaluates whether {@code current} is a better next-token candidate than
237     * the existing {@code candidate} relative to {@code rightBracket}.
238     * A token qualifies as a better candidate when it sits on the same line as
239     * the right bracket, has a greater column number than the bracket, and
240     * either no candidate exists yet or its column number is closer to the
241     * bracket than the current best. When the criteria are met the new token
242     * is returned; otherwise the existing candidate is returned unchanged.
243     *
244     * @param candidate the current best candidate
245     * @param rightBracket the right bracket token
246     * @param current the current AST node being evaluated
247     * @return the new best candidate
248     */
249    @Nullable
250    private static DetailAST findBestCandidate(@Nullable DetailAST candidate,
251            DetailAST rightBracket, DetailAST current) {
252        DetailAST result = candidate;
253        final boolean newCandidate = current.getLineNo() == rightBracket.getLineNo()
254                && current.getColumnNo() > rightBracket.getColumnNo()
255                && (candidate == null
256                        || current.getColumnNo() < candidate.getColumnNo());
257        if (newCandidate) {
258            result = current;
259        }
260        return result;
261    }
262
263    /**
264     * Checks if the given token can follow a right bracket without whitespace.
265     * Uses TokenTypes to determine valid tokens.
266     *
267     * @param nextToken the token that follows the right bracket
268     * @return true if the token can follow without whitespace
269     */
270    private static boolean isValidWithoutWhitespace(DetailAST nextToken) {
271        final int type = nextToken.getType();
272
273        return VALID_AFTER_RIGHT_BRACKET_TOKENS.contains(type);
274    }
275
276}