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.6.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.COMMA,
104            TokenTypes.SEMI,
105            TokenTypes.RPAREN,
106            TokenTypes.GENERIC_END,
107            TokenTypes.POST_INC,
108            TokenTypes.POST_DEC
109        );
110
111    @Override
112    public int[] getDefaultTokens() {
113        return getRequiredTokens();
114    }
115
116    @Override
117    public int[] getAcceptableTokens() {
118        return getRequiredTokens();
119    }
120
121    @Override
122    public int[] getRequiredTokens() {
123        return new int[] {
124            TokenTypes.ARRAY_DECLARATOR,
125            TokenTypes.INDEX_OP,
126            TokenTypes.RBRACK,
127        };
128    }
129
130    @Override
131    public void visitToken(DetailAST ast) {
132        if (ast.getType() == TokenTypes.RBRACK) {
133            processRightBracket(ast);
134        }
135        else {
136            final boolean whitespaceBefore = isWhitespaceAt(ast, ast.getColumnNo() - 1);
137            final boolean annotationBefore = isPrecededByAnnotation(ast);
138            if (!annotationBefore && whitespaceBefore) {
139                log(ast, MSG_WS_PRECEDED, ast.getText());
140            }
141            else if (annotationBefore && !whitespaceBefore) {
142                log(ast, MSG_WS_NOT_PRECEDED, ast.getText());
143            }
144            if (isWhitespaceAt(ast, ast.getColumnNo() + 1)) {
145                log(ast, MSG_WS_FOLLOWED, ast.getText());
146            }
147        }
148    }
149
150    /**
151     * Processes a right bracket token and logs violations if it is preceded
152     * or followed by whitespace inappropriately.
153     *
154     * @param ast the right bracket token to process
155     */
156    private void processRightBracket(DetailAST ast) {
157        if (isWhitespaceAt(ast, ast.getColumnNo() - 1)) {
158            log(ast, MSG_WS_PRECEDED, ast.getText());
159        }
160
161        final DetailAST nextToken = findNextToken(ast);
162        if (nextToken != null) {
163            final boolean whitespaceAfter = isWhitespaceAt(ast, ast.getColumnNo() + 1);
164            final boolean requiresWhitespace = !isValidWithoutWhitespace(nextToken);
165            if (requiresWhitespace && !whitespaceAfter) {
166                log(ast, MSG_WS_NOT_FOLLOWED, ast.getText());
167            }
168            else if (!requiresWhitespace && whitespaceAfter) {
169                log(ast, MSG_WS_FOLLOWED, ast.getText());
170            }
171        }
172    }
173
174    /**
175     * Checks whether an {@code ARRAY_DECLARATOR} is immediately preceded by an
176     * {@code ANNOTATIONS} sibling, which happens in constructs like
177     * {@code int @Ann [] x}.
178     *
179     * @param ast the {@code ARRAY_DECLARATOR} or {@code INDEX_OP} token
180     * @return true if the token's previous sibling is an ANNOTATIONS node
181     */
182    private static boolean isPrecededByAnnotation(DetailAST ast) {
183        final DetailAST previousSibling = ast.getPreviousSibling();
184        return previousSibling != null
185                && previousSibling.getType() == TokenTypes.ANNOTATIONS;
186    }
187
188    /**
189     * Checks if a whitespace character is present at the given column on the
190     * same line as the provided token.
191     *
192     * @param token the token whose line should be checked
193     * @param columnNo the column number to inspect for whitespace
194     * @return true if the character at {@code columnNo} is a whitespace character
195     */
196    private boolean isWhitespaceAt(DetailAST token, int columnNo) {
197        final int[] line = getLineCodePoints(token.getLineNo() - 1);
198        return columnNo >= 0 && columnNo < line.length
199                && CommonUtil.isCodePointWhitespace(line, columnNo);
200    }
201
202    /**
203     * Finds the next token after a right bracket by climbing the AST and
204     * scanning next-sibling chains at each level. At every level all siblings
205     * are visited: siblings on a later line are skipped and set an
206     * {@code outOfLine} flag; siblings on the same line are passed to
207     * {@link #findBestCandidate}. Once a later-line sibling is found at any
208     * level the climb stops immediately, since no ancestor sibling can be on
209     * the target line either. The candidate with the smallest qualifying
210     * column is returned.
211     *
212     * @param rightBracket the right bracket token whose successor is needed
213     * @return the closest same-line token that follows the bracket, or {@code null}
214     *         if no such token exists on that line
215     */
216    @Nullable
217    private static DetailAST findNextToken(DetailAST rightBracket) {
218        DetailAST candidate = null;
219        DetailAST current = rightBracket;
220
221        while (current != null) {
222            for (DetailAST sibling = current; sibling != null; sibling = sibling.getNextSibling()) {
223                candidate = findBestCandidate(candidate, rightBracket, sibling);
224            }
225            current = current.getParent();
226        }
227        return candidate;
228    }
229
230    /**
231     * Evaluates whether {@code current} is a better next-token candidate than
232     * the existing {@code candidate} relative to {@code rightBracket}.
233     * A token qualifies as a better candidate when it sits on the same line as
234     * the right bracket, has a greater column number than the bracket, and
235     * either no candidate exists yet or its column number is closer to the
236     * bracket than the current best. When the criteria are met the new token
237     * is returned; otherwise the existing candidate is returned unchanged.
238     *
239     * @param candidate the current best candidate
240     * @param rightBracket the right bracket token
241     * @param current the current AST node being evaluated
242     * @return the new best candidate
243     */
244    @Nullable
245    private static DetailAST findBestCandidate(@Nullable DetailAST candidate,
246            DetailAST rightBracket, DetailAST current) {
247        DetailAST result = candidate;
248        final boolean newCandidate = current.getLineNo() == rightBracket.getLineNo()
249                && current.getColumnNo() > rightBracket.getColumnNo()
250                && (candidate == null
251                        || current.getColumnNo() < candidate.getColumnNo());
252        if (newCandidate) {
253            result = current;
254        }
255        return result;
256    }
257
258    /**
259     * Checks if the given token can follow a right bracket without whitespace.
260     * Uses TokenTypes to determine valid tokens.
261     *
262     * @param nextToken the token that follows the right bracket
263     * @return true if the token can follow without whitespace
264     */
265    private static boolean isValidWithoutWhitespace(DetailAST nextToken) {
266        final int type = nextToken.getType();
267
268        return VALID_AFTER_RIGHT_BRACKET_TOKENS.contains(type);
269    }
270}