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