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