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}