001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.stream.IntStream; 023 024import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 025import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 026import com.puppycrawl.tools.checkstyle.api.DetailAST; 027import com.puppycrawl.tools.checkstyle.api.TokenTypes; 028import com.puppycrawl.tools.checkstyle.utils.CodePointUtil; 029import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 030 031/** 032 * <div> 033 * Checks that the whitespace around the Generic tokens (angle brackets) 034 * "<" and ">" are correct to the <i>typical</i> convention. 035 * The convention is not configurable. 036 * </div> 037 * 038 * <p> 039 * Left angle bracket ("<"): 040 * </p> 041 * <ul> 042 * <li> should be preceded with whitespace only 043 * in generic methods definitions.</li> 044 * <li> should not be preceded with whitespace 045 * when it is preceded method name or constructor.</li> 046 * <li> should not be preceded with whitespace when following type name.</li> 047 * <li> should not be followed with whitespace in all cases.</li> 048 * </ul> 049 * 050 * <p> 051 * Right angle bracket (">"): 052 * </p> 053 * <ul> 054 * <li> should not be preceded with whitespace in all cases.</li> 055 * <li> should be followed with whitespace in almost all cases, 056 * except diamond operators and when preceding a method name, constructor, or record header.</li> 057 * </ul> 058 * 059 * @since 5.0 060 */ 061@FileStatefulCheck 062public class GenericWhitespaceCheck extends AbstractCheck { 063 064 /** 065 * A key is pointing to the warning message text in "messages.properties" 066 * file. 067 */ 068 public static final String MSG_WS_PRECEDED = "ws.preceded"; 069 070 /** 071 * A key is pointing to the warning message text in "messages.properties" 072 * file. 073 */ 074 public static final String MSG_WS_FOLLOWED = "ws.followed"; 075 076 /** 077 * A key is pointing to the warning message text in "messages.properties" 078 * file. 079 */ 080 public static final String MSG_WS_NOT_PRECEDED = "ws.notPreceded"; 081 082 /** 083 * A key is pointing to the warning message text in "messages.properties" 084 * file. 085 */ 086 public static final String MSG_WS_ILLEGAL_FOLLOW = "ws.illegalFollow"; 087 088 /** Open angle bracket literal. */ 089 private static final String OPEN_ANGLE_BRACKET = "<"; 090 091 /** Close angle bracket literal. */ 092 private static final String CLOSE_ANGLE_BRACKET = ">"; 093 094 /** Used to count the depth of a Generic expression. */ 095 private int depth; 096 097 @Override 098 public int[] getDefaultTokens() { 099 return getRequiredTokens(); 100 } 101 102 @Override 103 public int[] getAcceptableTokens() { 104 return getRequiredTokens(); 105 } 106 107 @Override 108 public int[] getRequiredTokens() { 109 return new int[] {TokenTypes.GENERIC_START, TokenTypes.GENERIC_END}; 110 } 111 112 @Override 113 public void beginTree(DetailAST rootAST) { 114 // Reset for each tree, just increase there are violations in preceding 115 // trees. 116 depth = 0; 117 } 118 119 @Override 120 public void visitToken(DetailAST ast) { 121 switch (ast.getType()) { 122 case TokenTypes.GENERIC_START -> { 123 processStart(ast); 124 depth++; 125 } 126 case TokenTypes.GENERIC_END -> { 127 processEnd(ast); 128 depth--; 129 } 130 default -> throw new IllegalArgumentException("Unknown type " + ast); 131 } 132 } 133 134 /** 135 * Checks the token for the end of Generics. 136 * 137 * @param ast the token to check 138 */ 139 private void processEnd(DetailAST ast) { 140 final int[] line = getLineCodePoints(ast.getLineNo() - 1); 141 final int before = ast.getColumnNo() - 1; 142 final int after = ast.getColumnNo() + 1; 143 144 if (before >= 0 && CommonUtil.isCodePointWhitespace(line, before) 145 && !containsWhitespaceBefore(before, line)) { 146 log(ast, MSG_WS_PRECEDED, CLOSE_ANGLE_BRACKET); 147 } 148 149 if (after < line.length) { 150 // Check if the last Generic, in which case must be a whitespace 151 // or a '(),[.'. 152 if (depth == 1) { 153 processSingleGeneric(ast, line, after); 154 } 155 else { 156 processNestedGenerics(ast, line, after); 157 } 158 } 159 } 160 161 /** 162 * Process Nested generics. 163 * 164 * @param ast token 165 * @param line unicode code points array of line 166 * @param after position after 167 */ 168 private void processNestedGenerics(DetailAST ast, int[] line, int after) { 169 // In a nested Generic type, so can only be a '>' or ',' or '&' 170 171 // In case of several extends definitions: 172 // 173 // class IntEnumValueType<E extends Enum<E> & IntEnum> 174 // ^ 175 // should be whitespace if followed by & -+ 176 // 177 final int indexOfAmp = IntStream.range(after, line.length) 178 .filter(index -> line[index] == '&') 179 .findFirst() 180 .orElse(-1); 181 if (indexOfAmp >= 1 182 && containsWhitespaceBetween(after, indexOfAmp, line)) { 183 if (indexOfAmp - after == 0) { 184 log(ast, MSG_WS_NOT_PRECEDED, "&"); 185 } 186 else if (indexOfAmp - after != 1) { 187 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET); 188 } 189 } 190 else if (line[after] == ' ') { 191 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET); 192 } 193 } 194 195 /** 196 * Process Single-generic. 197 * 198 * @param ast token 199 * @param line unicode code points array of line 200 * @param after position after 201 */ 202 private void processSingleGeneric(DetailAST ast, int[] line, int after) { 203 final char charAfter = Character.toChars(line[after])[0]; 204 if (isGenericBeforeMethod(ast) 205 || isGenericBeforeCtorInvocation(ast) 206 || isGenericBeforeRecordHeader(ast)) { 207 if (Character.isWhitespace(charAfter)) { 208 log(ast, MSG_WS_FOLLOWED, CLOSE_ANGLE_BRACKET); 209 } 210 } 211 else if (!isCharacterValidAfterGenericEnd(charAfter)) { 212 log(ast, MSG_WS_ILLEGAL_FOLLOW, CLOSE_ANGLE_BRACKET); 213 } 214 } 215 216 /** 217 * Checks if generic is before record header. Identifies two cases: 218 * <ol> 219 * <li>In record def, eg: {@code record Session<T>()}</li> 220 * <li>In record pattern def, eg: {@code o instanceof Session<String>(var s)}</li> 221 * </ol> 222 * 223 * @param ast ast 224 * @return true if generic is before record header 225 */ 226 private static boolean isGenericBeforeRecordHeader(DetailAST ast) { 227 final DetailAST grandParent = ast.getParent().getParent(); 228 return grandParent.getType() == TokenTypes.RECORD_DEF 229 || grandParent.getParent().getType() == TokenTypes.RECORD_PATTERN_DEF; 230 } 231 232 /** 233 * Checks if generic is before constructor invocation. Identifies two cases: 234 * <ol> 235 * <li>{@code new ArrayList<>();}</li> 236 * <li>{@code new Outer.Inner<>();}</li> 237 * </ol> 238 * 239 * @param ast ast 240 * @return true if generic is before constructor invocation 241 */ 242 private static boolean isGenericBeforeCtorInvocation(DetailAST ast) { 243 final DetailAST grandParent = ast.getParent().getParent(); 244 return grandParent.getType() == TokenTypes.LITERAL_NEW 245 || grandParent.getParent().getType() == TokenTypes.LITERAL_NEW; 246 } 247 248 /** 249 * Checks if generic is after {@code LITERAL_NEW}. Identifies three cases: 250 * <ol> 251 * <li>{@code new <String>Object();}</li> 252 * <li>{@code new <String>Outer.Inner();}</li> 253 * <li>{@code new <@A Outer>@B Inner();}</li> 254 * </ol> 255 * 256 * @param ast ast 257 * @return true if generic after {@code LITERAL_NEW} 258 */ 259 private static boolean isGenericAfterNew(DetailAST ast) { 260 final DetailAST parent = ast.getParent(); 261 return parent.getParent().getType() == TokenTypes.LITERAL_NEW 262 && (parent.getNextSibling().getType() == TokenTypes.IDENT 263 || parent.getNextSibling().getType() == TokenTypes.DOT 264 || parent.getNextSibling().getType() == TokenTypes.ANNOTATIONS); 265 } 266 267 /** 268 * Is generic before method reference. 269 * 270 * @param ast ast 271 * @return true if generic before a method ref 272 */ 273 private static boolean isGenericBeforeMethod(DetailAST ast) { 274 return ast.getParent().getParent().getParent().getType() == TokenTypes.METHOD_CALL 275 || isAfterMethodReference(ast); 276 } 277 278 /** 279 * Checks if current generic end ('>') is located after 280 * {@link TokenTypes#METHOD_REF method reference operator}. 281 * 282 * @param genericEnd {@link TokenTypes#GENERIC_END} 283 * @return true if '>' follows after method reference. 284 */ 285 private static boolean isAfterMethodReference(DetailAST genericEnd) { 286 return genericEnd.getParent().getParent().getType() == TokenTypes.METHOD_REF; 287 } 288 289 /** 290 * Checks the token for the start of Generics. 291 * 292 * @param ast the token to check 293 */ 294 private void processStart(DetailAST ast) { 295 final int[] line = getLineCodePoints(ast.getLineNo() - 1); 296 final int before = ast.getColumnNo() - 1; 297 final int after = ast.getColumnNo() + 1; 298 299 // Checks if generic needs to be preceded by a whitespace or not. 300 // Handles 3 cases as in: 301 // 302 // public static <T> Callable<T> callable(Runnable task, T result) 303 // ^ ^ 304 // 1. ws reqd ---+ 2. +--- whitespace NOT required 305 // 306 // new <String>Object() 307 // ^ 308 // 3. +--- ws required 309 if (before >= 0) { 310 final DetailAST parent = ast.getParent(); 311 final DetailAST grandparent = parent.getParent(); 312 // cases (1, 3) where whitespace is required: 313 if (grandparent.getType() == TokenTypes.CTOR_DEF 314 || grandparent.getType() == TokenTypes.METHOD_DEF 315 || isGenericAfterNew(ast)) { 316 317 if (!CommonUtil.isCodePointWhitespace(line, before)) { 318 log(ast, MSG_WS_NOT_PRECEDED, OPEN_ANGLE_BRACKET); 319 } 320 } 321 // case 2 where whitespace is not required: 322 else if (CommonUtil.isCodePointWhitespace(line, before) 323 && !containsWhitespaceBefore(before, line)) { 324 log(ast, MSG_WS_PRECEDED, OPEN_ANGLE_BRACKET); 325 } 326 } 327 328 if (after < line.length 329 && CommonUtil.isCodePointWhitespace(line, after)) { 330 log(ast, MSG_WS_FOLLOWED, OPEN_ANGLE_BRACKET); 331 } 332 } 333 334 /** 335 * Returns whether the specified string contains only whitespace between 336 * specified indices. 337 * 338 * @param fromIndex the index to start the search from. Inclusive 339 * @param toIndex the index to finish the search. Exclusive 340 * @param line the unicode code points array of line to check 341 * @return whether there are only whitespaces (or nothing) 342 */ 343 private static boolean containsWhitespaceBetween(int fromIndex, int toIndex, int... line) { 344 boolean result = true; 345 for (int i = fromIndex; i < toIndex; i++) { 346 if (!CommonUtil.isCodePointWhitespace(line, i)) { 347 result = false; 348 break; 349 } 350 } 351 return result; 352 } 353 354 /** 355 * Returns whether the specified string contains only whitespace up to specified index. 356 * 357 * @param before the index to finish the search. Exclusive 358 * @param line the unicode code points array of line to check 359 * @return {@code true} if there are only whitespaces, 360 * false if there is nothing before or some other characters 361 */ 362 private static boolean containsWhitespaceBefore(int before, int... line) { 363 return before != 0 && CodePointUtil.hasWhitespaceBefore(before, line); 364 } 365 366 /** 367 * Checks whether given character is valid to be right after generic ends. 368 * 369 * @param charAfter character to check 370 * @return checks if given character is valid 371 */ 372 private static boolean isCharacterValidAfterGenericEnd(char charAfter) { 373 return charAfter == ')' || charAfter == ',' 374 || charAfter == '[' || charAfter == '.' 375 || charAfter == ':' || charAfter == ';' 376 || Character.isWhitespace(charAfter); 377 } 378 379}