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.Optional; 023 024import com.puppycrawl.tools.checkstyle.StatelessCheck; 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.CommonUtil; 029 030/** 031 * <p> 032 * Checks that there is no whitespace after a token. 033 * More specifically, it checks that it is not followed by whitespace, 034 * or (if linebreaks are allowed) all characters on the line after are 035 * whitespace. To forbid linebreaks after a token, set property 036 * {@code allowLineBreaks} to {@code false}. 037 * </p> 038 * <p> 039 * The check processes 040 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR"> 041 * ARRAY_DECLARATOR</a> and 042 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP"> 043 * INDEX_OP</a> tokens specially from other tokens. Actually it is checked that 044 * there is no whitespace before these tokens, not after them. Space after the 045 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ANNOTATIONS"> 046 * ANNOTATIONS</a> before 047 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR"> 048 * ARRAY_DECLARATOR</a> and 049 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP"> 050 * INDEX_OP</a> will be ignored. 051 * </p> 052 * <p> 053 * If the annotation is between the type and the array, like {@code char @NotNull [] param}, 054 * the check will skip validation for spaces. 055 * </p> 056 * <p> 057 * Note: This check processes the 058 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_SYNCHRONIZED"> 059 * LITERAL_SYNCHRONIZED</a> token only when it appears as a part of a 060 * <a href="https://docs.oracle.com/javase/specs/jls/se19/html/jls-14.html#jls-14.19"> 061 * synchronized statement</a>, i.e. {@code synchronized(this) {}}. 062 * </p> 063 * <ul> 064 * <li> 065 * Property {@code allowLineBreaks} - Control whether whitespace is allowed 066 * if the token is at a linebreak. 067 * Type is {@code boolean}. 068 * Default value is {@code true}. 069 * </li> 070 * <li> 071 * Property {@code tokens} - tokens to check 072 * Type is {@code java.lang.String[]}. 073 * Validation type is {@code tokenSet}. 074 * Default value is: 075 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_INIT"> 076 * ARRAY_INIT</a>, 077 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#AT"> 078 * AT</a>, 079 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INC"> 080 * INC</a>, 081 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#DEC"> 082 * DEC</a>, 083 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#UNARY_MINUS"> 084 * UNARY_MINUS</a>, 085 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#UNARY_PLUS"> 086 * UNARY_PLUS</a>, 087 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BNOT"> 088 * BNOT</a>, 089 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LNOT"> 090 * LNOT</a>, 091 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#DOT"> 092 * DOT</a>, 093 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#ARRAY_DECLARATOR"> 094 * ARRAY_DECLARATOR</a>, 095 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INDEX_OP"> 096 * INDEX_OP</a>. 097 * </li> 098 * </ul> 099 * <p> 100 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 101 * </p> 102 * <p> 103 * Violation Message Keys: 104 * </p> 105 * <ul> 106 * <li> 107 * {@code ws.followed} 108 * </li> 109 * </ul> 110 * 111 * @since 3.0 112 */ 113@StatelessCheck 114public class NoWhitespaceAfterCheck extends AbstractCheck { 115 116 /** 117 * A key is pointing to the warning message text in "messages.properties" 118 * file. 119 */ 120 public static final String MSG_KEY = "ws.followed"; 121 122 /** Control whether whitespace is allowed if the token is at a linebreak. */ 123 private boolean allowLineBreaks = true; 124 125 @Override 126 public int[] getDefaultTokens() { 127 return new int[] { 128 TokenTypes.ARRAY_INIT, 129 TokenTypes.AT, 130 TokenTypes.INC, 131 TokenTypes.DEC, 132 TokenTypes.UNARY_MINUS, 133 TokenTypes.UNARY_PLUS, 134 TokenTypes.BNOT, 135 TokenTypes.LNOT, 136 TokenTypes.DOT, 137 TokenTypes.ARRAY_DECLARATOR, 138 TokenTypes.INDEX_OP, 139 }; 140 } 141 142 @Override 143 public int[] getAcceptableTokens() { 144 return new int[] { 145 TokenTypes.ARRAY_INIT, 146 TokenTypes.AT, 147 TokenTypes.INC, 148 TokenTypes.DEC, 149 TokenTypes.UNARY_MINUS, 150 TokenTypes.UNARY_PLUS, 151 TokenTypes.BNOT, 152 TokenTypes.LNOT, 153 TokenTypes.DOT, 154 TokenTypes.TYPECAST, 155 TokenTypes.ARRAY_DECLARATOR, 156 TokenTypes.INDEX_OP, 157 TokenTypes.LITERAL_SYNCHRONIZED, 158 TokenTypes.METHOD_REF, 159 }; 160 } 161 162 @Override 163 public int[] getRequiredTokens() { 164 return CommonUtil.EMPTY_INT_ARRAY; 165 } 166 167 /** 168 * Setter to control whether whitespace is allowed if the token is at a linebreak. 169 * 170 * @param allowLineBreaks whether whitespace should be 171 * flagged at linebreaks. 172 * @since 3.0 173 */ 174 public void setAllowLineBreaks(boolean allowLineBreaks) { 175 this.allowLineBreaks = allowLineBreaks; 176 } 177 178 @Override 179 public void visitToken(DetailAST ast) { 180 if (shouldCheckWhitespaceAfter(ast)) { 181 final DetailAST whitespaceFollowedAst = getWhitespaceFollowedNode(ast); 182 final int whitespaceColumnNo = getPositionAfter(whitespaceFollowedAst); 183 final int whitespaceLineNo = whitespaceFollowedAst.getLineNo(); 184 185 if (hasTrailingWhitespace(ast, whitespaceColumnNo, whitespaceLineNo)) { 186 log(ast, MSG_KEY, whitespaceFollowedAst.getText()); 187 } 188 } 189 } 190 191 /** 192 * For a visited ast node returns node that should be checked 193 * for not being followed by whitespace. 194 * 195 * @param ast 196 * , visited node. 197 * @return node before ast. 198 */ 199 private static DetailAST getWhitespaceFollowedNode(DetailAST ast) { 200 final DetailAST whitespaceFollowedAst; 201 switch (ast.getType()) { 202 case TokenTypes.TYPECAST: 203 whitespaceFollowedAst = ast.findFirstToken(TokenTypes.RPAREN); 204 break; 205 case TokenTypes.ARRAY_DECLARATOR: 206 whitespaceFollowedAst = getArrayDeclaratorPreviousElement(ast); 207 break; 208 case TokenTypes.INDEX_OP: 209 whitespaceFollowedAst = getIndexOpPreviousElement(ast); 210 break; 211 default: 212 whitespaceFollowedAst = ast; 213 } 214 return whitespaceFollowedAst; 215 } 216 217 /** 218 * Returns whether whitespace after a visited node should be checked. For example, whitespace 219 * is not allowed between a type and an array declarator (returns true), except when there is 220 * an annotation in between the type and array declarator (returns false). 221 * 222 * @param ast the visited node 223 * @return true if whitespace after ast should be checked 224 */ 225 private static boolean shouldCheckWhitespaceAfter(DetailAST ast) { 226 final DetailAST previousSibling = ast.getPreviousSibling(); 227 final boolean isSynchronizedMethod = ast.getType() == TokenTypes.LITERAL_SYNCHRONIZED 228 && ast.getFirstChild() == null; 229 return !isSynchronizedMethod 230 && (previousSibling == null || previousSibling.getType() != TokenTypes.ANNOTATIONS); 231 } 232 233 /** 234 * Gets position after token (place of possible redundant whitespace). 235 * 236 * @param ast Node representing token. 237 * @return position after token. 238 */ 239 private static int getPositionAfter(DetailAST ast) { 240 final int after; 241 // If target of possible redundant whitespace is in method definition. 242 if (ast.getType() == TokenTypes.IDENT 243 && ast.getNextSibling() != null 244 && ast.getNextSibling().getType() == TokenTypes.LPAREN) { 245 final DetailAST methodDef = ast.getParent(); 246 final DetailAST endOfParams = methodDef.findFirstToken(TokenTypes.RPAREN); 247 after = endOfParams.getColumnNo() + 1; 248 } 249 else { 250 after = ast.getColumnNo() + ast.getText().length(); 251 } 252 return after; 253 } 254 255 /** 256 * Checks if there is unwanted whitespace after the visited node. 257 * 258 * @param ast 259 * , visited node. 260 * @param whitespaceColumnNo 261 * , column number of a possible whitespace. 262 * @param whitespaceLineNo 263 * , line number of a possible whitespace. 264 * @return true if whitespace found. 265 */ 266 private boolean hasTrailingWhitespace(DetailAST ast, 267 int whitespaceColumnNo, int whitespaceLineNo) { 268 final boolean result; 269 final int astLineNo = ast.getLineNo(); 270 final int[] line = getLineCodePoints(astLineNo - 1); 271 if (astLineNo == whitespaceLineNo && whitespaceColumnNo < line.length) { 272 result = CommonUtil.isCodePointWhitespace(line, whitespaceColumnNo); 273 } 274 else { 275 result = !allowLineBreaks; 276 } 277 return result; 278 } 279 280 /** 281 * Returns proper argument for getPositionAfter method, it is a token after 282 * {@link TokenTypes#ARRAY_DECLARATOR ARRAY_DECLARATOR}, in can be {@link TokenTypes#RBRACK 283 * RBRACK}, {@link TokenTypes#IDENT IDENT} or an array type definition (literal). 284 * 285 * @param ast 286 * , {@link TokenTypes#ARRAY_DECLARATOR ARRAY_DECLARATOR} node. 287 * @return previous node by text order. 288 * @throws IllegalStateException if an unexpected token type is encountered. 289 */ 290 private static DetailAST getArrayDeclaratorPreviousElement(DetailAST ast) { 291 final DetailAST previousElement; 292 293 if (ast.getPreviousSibling() != null 294 && ast.getPreviousSibling().getType() == TokenTypes.ARRAY_DECLARATOR) { 295 // Covers higher dimension array declarations and initializations 296 previousElement = getPreviousElementOfMultiDimArray(ast); 297 } 298 else { 299 // first array index, is preceded with identifier or type 300 final DetailAST parent = ast.getParent(); 301 switch (parent.getType()) { 302 // generics 303 case TokenTypes.TYPE_UPPER_BOUNDS: 304 case TokenTypes.TYPE_LOWER_BOUNDS: 305 previousElement = ast.getPreviousSibling(); 306 break; 307 case TokenTypes.LITERAL_NEW: 308 case TokenTypes.TYPE_ARGUMENT: 309 case TokenTypes.DOT: 310 previousElement = getTypeLastNode(ast); 311 break; 312 // mundane array declaration, can be either java style or C style 313 case TokenTypes.TYPE: 314 previousElement = getPreviousNodeWithParentOfTypeAst(ast, parent); 315 break; 316 // java 8 method reference 317 case TokenTypes.METHOD_REF: 318 final DetailAST ident = getIdentLastToken(ast); 319 if (ident == null) { 320 // i.e. int[]::new 321 previousElement = ast.getParent().getFirstChild(); 322 } 323 else { 324 previousElement = ident; 325 } 326 break; 327 default: 328 throw new IllegalStateException("unexpected ast syntax " + parent); 329 } 330 } 331 return previousElement; 332 } 333 334 /** 335 * Gets the previous element of a second or higher dimension of an 336 * array declaration or initialization. 337 * 338 * @param leftBracket the token to get previous element of 339 * @return the previous element 340 */ 341 private static DetailAST getPreviousElementOfMultiDimArray(DetailAST leftBracket) { 342 final DetailAST previousRightBracket = leftBracket.getPreviousSibling().getLastChild(); 343 344 DetailAST ident = null; 345 // This will get us past the type ident, to the actual identifier 346 DetailAST parent = leftBracket.getParent().getParent(); 347 while (ident == null) { 348 ident = parent.findFirstToken(TokenTypes.IDENT); 349 parent = parent.getParent(); 350 } 351 352 final DetailAST previousElement; 353 if (ident.getColumnNo() > previousRightBracket.getColumnNo() 354 && ident.getColumnNo() < leftBracket.getColumnNo()) { 355 // C style and Java style ' int[] arr []' in same construct 356 previousElement = ident; 357 } 358 else { 359 // 'int[][] arr' or 'int arr[][]' 360 previousElement = previousRightBracket; 361 } 362 return previousElement; 363 } 364 365 /** 366 * Gets previous node for {@link TokenTypes#INDEX_OP INDEX_OP} token 367 * for usage in getPositionAfter method, it is a simplified copy of 368 * getArrayDeclaratorPreviousElement method. 369 * 370 * @param ast 371 * , {@link TokenTypes#INDEX_OP INDEX_OP} node. 372 * @return previous node by text order. 373 */ 374 private static DetailAST getIndexOpPreviousElement(DetailAST ast) { 375 final DetailAST result; 376 final DetailAST firstChild = ast.getFirstChild(); 377 if (firstChild.getType() == TokenTypes.INDEX_OP) { 378 // second or higher array index 379 result = firstChild.findFirstToken(TokenTypes.RBRACK); 380 } 381 else if (firstChild.getType() == TokenTypes.IDENT) { 382 result = firstChild; 383 } 384 else { 385 final DetailAST ident = getIdentLastToken(ast); 386 if (ident == null) { 387 final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN); 388 // construction like new int[]{1}[0] 389 if (rparen == null) { 390 final DetailAST lastChild = firstChild.getLastChild(); 391 result = lastChild.findFirstToken(TokenTypes.RCURLY); 392 } 393 // construction like ((byte[]) pixels)[0] 394 else { 395 result = rparen; 396 } 397 } 398 else { 399 result = ident; 400 } 401 } 402 return result; 403 } 404 405 /** 406 * Searches parameter node for a type node. 407 * Returns it or its last node if it has an extended structure. 408 * 409 * @param ast 410 * , subject node. 411 * @return type node. 412 */ 413 private static DetailAST getTypeLastNode(DetailAST ast) { 414 final DetailAST typeLastNode; 415 final DetailAST parent = ast.getParent(); 416 final boolean isPrecededByTypeArgs = 417 parent.findFirstToken(TokenTypes.TYPE_ARGUMENTS) != null; 418 final Optional<DetailAST> objectArrayType = Optional.ofNullable(getIdentLastToken(ast)); 419 420 if (isPrecededByTypeArgs) { 421 typeLastNode = parent.findFirstToken(TokenTypes.TYPE_ARGUMENTS) 422 .findFirstToken(TokenTypes.GENERIC_END); 423 } 424 else if (objectArrayType.isPresent()) { 425 typeLastNode = objectArrayType.orElseThrow(); 426 } 427 else { 428 typeLastNode = parent.getFirstChild(); 429 } 430 431 return typeLastNode; 432 } 433 434 /** 435 * Finds previous node by text order for an array declarator, 436 * which parent type is {@link TokenTypes#TYPE TYPE}. 437 * 438 * @param ast 439 * , array declarator node. 440 * @param parent 441 * , its parent node. 442 * @return previous node by text order. 443 */ 444 private static DetailAST getPreviousNodeWithParentOfTypeAst(DetailAST ast, DetailAST parent) { 445 final DetailAST previousElement; 446 final DetailAST ident = getIdentLastToken(parent.getParent()); 447 final DetailAST lastTypeNode = getTypeLastNode(ast); 448 // sometimes there are ident-less sentences 449 // i.e. "(Object[]) null", but in casual case should be 450 // checked whether ident or lastTypeNode has preceding position 451 // determining if it is java style or C style 452 453 if (ident == null || ident.getLineNo() > ast.getLineNo()) { 454 previousElement = lastTypeNode; 455 } 456 else if (ident.getLineNo() < ast.getLineNo()) { 457 previousElement = ident; 458 } 459 // ident and lastTypeNode lay on one line 460 else { 461 final int instanceOfSize = 13; 462 // +2 because ast has `[]` after the ident 463 if (ident.getColumnNo() >= ast.getColumnNo() + 2 464 // +13 because ident (at most 1 character) is followed by 465 // ' instanceof ' (12 characters) 466 || lastTypeNode.getColumnNo() >= ident.getColumnNo() + instanceOfSize) { 467 previousElement = lastTypeNode; 468 } 469 else { 470 previousElement = ident; 471 } 472 } 473 return previousElement; 474 } 475 476 /** 477 * Gets leftmost token of identifier. 478 * 479 * @param ast 480 * , token possibly possessing an identifier. 481 * @return leftmost token of identifier. 482 */ 483 private static DetailAST getIdentLastToken(DetailAST ast) { 484 final DetailAST result; 485 final Optional<DetailAST> dot = getPrecedingDot(ast); 486 // method call case 487 if (dot.isEmpty() || ast.getFirstChild().getType() == TokenTypes.METHOD_CALL) { 488 final DetailAST methodCall = ast.findFirstToken(TokenTypes.METHOD_CALL); 489 if (methodCall == null) { 490 result = ast.findFirstToken(TokenTypes.IDENT); 491 } 492 else { 493 result = methodCall.findFirstToken(TokenTypes.RPAREN); 494 } 495 } 496 // qualified name case 497 else { 498 result = dot.orElseThrow().getFirstChild().getNextSibling(); 499 } 500 return result; 501 } 502 503 /** 504 * Gets the dot preceding a class member array index operation or class 505 * reference. 506 * 507 * @param leftBracket the ast we are checking 508 * @return dot preceding the left bracket 509 */ 510 private static Optional<DetailAST> getPrecedingDot(DetailAST leftBracket) { 511 final DetailAST referencedMemberDot = leftBracket.findFirstToken(TokenTypes.DOT); 512 final Optional<DetailAST> result = Optional.ofNullable(referencedMemberDot); 513 return result.or(() -> getReferencedClassDot(leftBracket)); 514 } 515 516 /** 517 * Gets the dot preceding a class reference. 518 * 519 * @param leftBracket the ast we are checking 520 * @return dot preceding the left bracket 521 */ 522 private static Optional<DetailAST> getReferencedClassDot(DetailAST leftBracket) { 523 final DetailAST parent = leftBracket.getParent(); 524 Optional<DetailAST> classDot = Optional.empty(); 525 if (parent.getType() != TokenTypes.ASSIGN) { 526 classDot = Optional.ofNullable(parent.findFirstToken(TokenTypes.DOT)); 527 } 528 return classDot; 529 } 530}