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