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