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