001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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 javax.annotation.Nullable; 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; 029import com.puppycrawl.tools.checkstyle.utils.NullUtil; 030import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 031 032/** 033 * <div> 034 * Checks that a whitespace is present before the opening brace of an empty body. 035 * </div> 036 * 037 * <p> 038 * This check applies to the following tokens: 039 * Methods and constructors, Classes, interfaces, enums, and records, 040 * Annotation definitions, Loops: {@code while}, {@code for}, {@code do-while}, 041 * Lambdas and anonymous classes, Static initializer blocks, 042 * Try-catch-finally statements, Synchronized blocks, Switch statements. 043 * </p> 044 * 045 * <p> 046 * A body containing only comments is considered empty by this check. 047 * </p> 048 * 049 * @since 13.9.0 050 */ 051@StatelessCheck 052public class WhitespaceBeforeEmptyBodyCheck extends AbstractCheck { 053 054 /** 055 * A key is pointing to the warning message text in "messages.properties" file. 056 */ 057 public static final String MSG_KEY = "ws.notPreceded"; 058 059 /** 060 * Creates a new {@code WhitespaceBeforeEmptyBodyCheck} instance. 061 */ 062 public WhitespaceBeforeEmptyBodyCheck() { 063 // no code by default 064 } 065 066 @Override 067 public int[] getDefaultTokens() { 068 return getAcceptableTokens(); 069 } 070 071 @Override 072 public int[] getAcceptableTokens() { 073 return new int[] { 074 TokenTypes.METHOD_DEF, 075 TokenTypes.CTOR_DEF, 076 TokenTypes.COMPACT_CTOR_DEF, 077 TokenTypes.CLASS_DEF, 078 TokenTypes.INTERFACE_DEF, 079 TokenTypes.RECORD_DEF, 080 TokenTypes.ENUM_DEF, 081 TokenTypes.ANNOTATION_DEF, 082 TokenTypes.LITERAL_NEW, 083 TokenTypes.LITERAL_DO, 084 TokenTypes.LITERAL_WHILE, 085 TokenTypes.LITERAL_FOR, 086 TokenTypes.LITERAL_IF, 087 TokenTypes.LITERAL_ELSE, 088 TokenTypes.STATIC_INIT, 089 TokenTypes.LITERAL_TRY, 090 TokenTypes.LITERAL_CATCH, 091 TokenTypes.LITERAL_FINALLY, 092 TokenTypes.LITERAL_SYNCHRONIZED, 093 TokenTypes.LITERAL_SWITCH, 094 TokenTypes.LAMBDA, 095 }; 096 } 097 098 @Override 099 public int[] getRequiredTokens() { 100 return CommonUtil.EMPTY_INT_ARRAY; 101 } 102 103 @Override 104 public void visitToken(DetailAST ast) { 105 final DetailAST lcurly = findOpeningBrace(ast); 106 if (lcurly != null && isBodyEmpty(lcurly) && !precededByWhiteSpace(lcurly)) { 107 log(lcurly, MSG_KEY, lcurly.getText()); 108 } 109 } 110 111 /** 112 * Returns left curly token for an ast. 113 * 114 * @param ast the AST to get brace token for 115 * @return the left curly token, or null if the construct has no-body 116 */ 117 @Nullable 118 private static DetailAST findOpeningBrace(DetailAST ast) { 119 final int astType = ast.getType(); 120 return switch (astType) { 121 case TokenTypes.CLASS_DEF, 122 TokenTypes.INTERFACE_DEF, 123 TokenTypes.ENUM_DEF, 124 TokenTypes.RECORD_DEF, 125 TokenTypes.ANNOTATION_DEF, 126 TokenTypes.LITERAL_NEW -> { 127 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK); 128 DetailAST lcurly = null; 129 if (objBlock != null) { 130 lcurly = objBlock.findFirstToken(TokenTypes.LCURLY); 131 } 132 yield lcurly; 133 } 134 case TokenTypes.LITERAL_SWITCH -> ast.findFirstToken(TokenTypes.LCURLY); 135 default -> ast.findFirstToken(TokenTypes.SLIST); 136 }; 137 } 138 139 /** 140 * Checks if the opening brace is preceded by whitespace. 141 * 142 * @param lcurly token representing the opening brace 143 * @return true if preceded by whitespace 144 */ 145 private static boolean precededByWhiteSpace(DetailAST lcurly) { 146 DetailAST previousToken = lcurly.getPreviousSibling(); 147 if (previousToken != null) { 148 previousToken = descendToLeafNode(previousToken); 149 } 150 if (previousToken == null 151 || previousToken.getParent().getType() == TokenTypes.LAMBDA) { 152 previousToken = NullUtil.notNull(lcurly.getParent()); 153 } 154 if (previousToken.getType() == TokenTypes.OBJBLOCK) { 155 previousToken = NullUtil.notNull(previousToken.getPreviousSibling()); 156 previousToken = descendToLeafNode(previousToken); 157 } 158 159 return !TokenUtil.areOnSameLine(lcurly, previousToken) 160 || isSpaceBetweenTokens(previousToken, lcurly); 161 } 162 163 /** 164 * Return the last nested child of the token. 165 * 166 * @param token the ast token 167 * @return nested child 168 */ 169 private static DetailAST descendToLeafNode(DetailAST token) { 170 DetailAST current = token; 171 while (current.hasChildren()) { 172 current = current.getLastChild(); 173 } 174 return current; 175 } 176 177 /** 178 * Checks if the body is empty. 179 * 180 * @param lcurly the token representing the opening brace 181 * @return true if the body is empty 182 */ 183 private static boolean isBodyEmpty(DetailAST lcurly) { 184 final DetailAST nextSibling = lcurly.getNextSibling(); 185 final DetailAST firstChild = lcurly.getFirstChild(); 186 return nextSibling != null && nextSibling.getType() == TokenTypes.RCURLY 187 || firstChild != null && firstChild.getType() == TokenTypes.RCURLY; 188 } 189 190 /** 191 * Checks if space exists between first and second token. 192 * 193 * @param firstToken token in ast which is behind second token 194 * @param secondToken token next to firstToken 195 * @return true if space exists between first and second tokens 196 */ 197 private static boolean isSpaceBetweenTokens(DetailAST firstToken, DetailAST secondToken) { 198 String tokenText = firstToken.getText(); 199 if (firstToken.getType() == TokenTypes.STATIC_INIT) { 200 tokenText = "static"; 201 } 202 final int firstLastColumn = firstToken.getColumnNo() 203 + tokenText.length() - 1; 204 return secondToken.getColumnNo() - firstLastColumn > 1; 205 } 206 207}