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}