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