View Javadoc
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 }