1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.whitespace;
21
22 import java.util.Set;
23
24 import javax.annotation.Nullable;
25
26 import com.puppycrawl.tools.checkstyle.StatelessCheck;
27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28 import com.puppycrawl.tools.checkstyle.api.DetailAST;
29 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31
32 /**
33 * <div>
34 * Checks that the whitespace around square-bracket tokens {@code [} and {@code ]}
35 * follows the Google Java Style Guide requirements for array declarations,
36 * array creation, and array indexing as described in
37 * <a href="https://google.github.io/styleguide/javaguide.html#s4.6.2-horizontal-whitespace">
38 * Section 4.6.2: Horizontal Whitespace</a>.
39 * </div>
40 *
41 * <p>
42 * Left square bracket ("{@code [}"):
43 * </p>
44 * <ul>
45 * <li>must not be preceded with whitespace when preceded by a
46 * {@code TYPE} or {@code IDENT} in array declarations or array access</li>
47 * <li>must not be followed with whitespace</li>
48 * </ul>
49 *
50 * <p>
51 * Right square bracket ("{@code ]}"):
52 * </p>
53 * <ul>
54 * <li>must not be preceded with whitespace</li>
55 * <li>must be followed with whitespace in all cases, except when followed by:
56 * <ul>
57 * <li>another bracket: {@code [][]}</li>
58 * <li>a dot for member access: {@code arr[i].length}</li>
59 * <li>a comma or semicolon: {@code arr[i],} or {@code arr[i];}</li>
60 * <li>postfix operators: {@code arr[i]++} or {@code arr[i]--}</li>
61 * <li>a right parenthesis or another closing construct: {@code (arr[i])}</li>
62 * </ul>
63 * </li>
64 * </ul>
65 *
66 * @since 13.10.0
67 */
68 @StatelessCheck
69 public class ArrayBracketNoWhitespaceCheck extends AbstractCheck {
70
71 /**
72 * A key is pointing to the warning message text in "messages.properties"
73 * file.
74 */
75 public static final String MSG_WS_PRECEDED = "ws.preceded";
76
77 /**
78 * A key is pointing to the warning message text in "messages.properties"
79 * file.
80 */
81 public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded";
82
83 /**
84 * A key is pointing to the warning message text in "messages.properties"
85 * file.
86 */
87 public static final String MSG_WS_FOLLOWED = "ws.followed";
88
89 /**
90 * A key is pointing to the warning message text in "messages.properties"
91 * file.
92 */
93 public static final String MSG_WS_NOT_FOLLOWED = "ws.notFollowed";
94
95 /**
96 * Tokens that are valid after a right bracket without whitespace.
97 */
98 private static final Set<Integer> VALID_AFTER_RIGHT_BRACKET_TOKENS =
99 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 }