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